hyrax 2.8.0 → 2.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +8 -8
- data/.gitignore +1 -0
- data/.regen +1 -1
- data/README.md +1 -1
- data/app/assets/javascripts/hyrax.js +1 -0
- data/app/assets/javascripts/hyrax/autocomplete.es6 +29 -0
- data/app/assets/javascripts/hyrax/editor.es6 +9 -10
- data/app/assets/javascripts/hyrax/skip_to_content.js +15 -0
- data/app/controllers/concerns/hyrax/works_controller_behavior.rb +18 -7
- data/app/controllers/hyrax/file_sets_controller.rb +6 -1
- data/app/controllers/hyrax/users_controller.rb +1 -1
- data/app/helpers/hyrax/hyrax_helper_behavior.rb +1 -0
- data/app/helpers/hyrax/work_form_helper.rb +48 -0
- data/app/jobs/embargo_expiry_job.rb +15 -0
- data/app/jobs/iiif_manifest_cache_prewarm_job.rb +16 -0
- data/app/jobs/lease_expiry_job.rb +15 -0
- data/app/models/concerns/hyrax/ability.rb +1 -1
- data/app/models/concerns/hyrax/solr_document/characterization.rb +1 -1
- data/app/models/concerns/hyrax/solr_document/metadata.rb +1 -0
- data/app/models/concerns/hyrax/solr_document/ordered_members.rb +46 -0
- data/app/models/concerns/hyrax/solr_document_behavior.rb +10 -0
- data/app/presenters/hyrax/displays_image.rb +25 -21
- data/app/presenters/hyrax/iiif_manifest_presenter.rb +232 -0
- data/app/presenters/hyrax/member_presenter_factory.rb +1 -7
- data/app/services/hyrax/caching_iiif_manifest_builder.rb +53 -0
- data/app/services/hyrax/collection_types/permissions_service.rb +3 -3
- data/app/services/hyrax/collections/permissions_service.rb +1 -1
- data/app/services/hyrax/contextual_path.rb +1 -1
- data/app/services/hyrax/identifier/builder.rb +45 -0
- data/app/services/hyrax/identifier/dispatcher.rb +61 -0
- data/app/services/hyrax/identifier/registrar.rb +41 -0
- data/app/services/hyrax/manifest_builder_service.rb +88 -0
- data/app/services/hyrax/versioning_service.rb +9 -0
- data/app/views/hyrax/base/_form.html.erb +1 -1
- data/app/views/hyrax/base/_form_child_work_relationships.html.erb +1 -1
- data/app/views/hyrax/base/_form_progress.html.erb +4 -0
- data/app/views/hyrax/base/_form_visibility_error.html.erb +2 -0
- data/app/views/hyrax/base/_guts4form.html.erb +7 -1
- data/app/views/hyrax/base/_show_actions.html.erb +1 -1
- data/app/views/hyrax/batch_uploads/_form.html.erb +2 -2
- data/app/views/hyrax/dashboard/_sidebar.html.erb +1 -1
- data/config/features.rb +4 -0
- data/hyrax.gemspec +3 -2
- data/lib/generators/hyrax/templates/catalog_controller.rb +4 -0
- data/lib/generators/hyrax/templates/config/initializers/hyrax.rb +5 -0
- data/lib/generators/hyrax/templates/config/locales/hyrax.es.yml +1 -1
- data/lib/hyrax.rb +1 -0
- data/lib/hyrax/configuration.rb +23 -4
- data/lib/hyrax/engine.rb +1 -0
- data/lib/hyrax/specs/shared_specs.rb +1 -0
- data/lib/hyrax/specs/shared_specs/identifiers.rb +27 -0
- data/lib/hyrax/version.rb +1 -1
- data/template.rb +1 -1
- metadata +47 -8
@@ -61,10 +61,20 @@ module Hyrax
|
|
61
61
|
@model ||= ModelWrapper.new(hydra_model, id)
|
62
62
|
end
|
63
63
|
|
64
|
+
##
|
65
|
+
# @return [Boolean]
|
64
66
|
def collection?
|
65
67
|
hydra_model == ::Collection
|
66
68
|
end
|
67
69
|
|
70
|
+
##
|
71
|
+
# @return [Boolean]
|
72
|
+
def file_set?
|
73
|
+
hydra_model == ::FileSet
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# @return [Boolean]
|
68
78
|
def admin_set?
|
69
79
|
hydra_model == ::AdminSet
|
70
80
|
end
|
@@ -11,20 +11,11 @@ module Hyrax
|
|
11
11
|
# @return [IIIFManifest::DisplayImage] the display image required by the manifest builder.
|
12
12
|
def display_image
|
13
13
|
return nil unless solr_document.image? && current_ability.can?(:read, solr_document)
|
14
|
-
|
15
|
-
latest_file_id = lookup_original_file_id
|
16
|
-
|
17
14
|
return nil unless latest_file_id
|
18
15
|
|
19
|
-
url = Hyrax.config.iiif_image_url_builder.call(
|
20
|
-
latest_file_id,
|
21
|
-
request.base_url,
|
22
|
-
Hyrax.config.iiif_image_size_default
|
23
|
-
)
|
24
|
-
|
25
16
|
# @see https://github.com/samvera-labs/iiif_manifest
|
26
|
-
IIIFManifest::DisplayImage.new(
|
27
|
-
format: image_format(
|
17
|
+
IIIFManifest::DisplayImage.new(display_image_url(request.base_url),
|
18
|
+
format: image_format(alpha_channels),
|
28
19
|
width: width,
|
29
20
|
height: height,
|
30
21
|
iiif_endpoint: iiif_endpoint(latest_file_id))
|
@@ -32,16 +23,24 @@ module Hyrax
|
|
32
23
|
|
33
24
|
private
|
34
25
|
|
35
|
-
def
|
26
|
+
def display_image_url(base_url)
|
27
|
+
Hyrax.config.iiif_image_url_builder.call(
|
28
|
+
latest_file_id,
|
29
|
+
base_url,
|
30
|
+
Hyrax.config.iiif_image_size_default
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def iiif_endpoint(file_id, base_url: request.base_url)
|
36
35
|
return unless Hyrax.config.iiif_image_server?
|
37
36
|
IIIFManifest::IIIFEndpoint.new(
|
38
|
-
Hyrax.config.iiif_info_url_builder.call(file_id,
|
37
|
+
Hyrax.config.iiif_info_url_builder.call(file_id, base_url),
|
39
38
|
profile: Hyrax.config.iiif_image_compliance_level_uri
|
40
39
|
)
|
41
40
|
end
|
42
41
|
|
43
42
|
def image_format(channels)
|
44
|
-
channels
|
43
|
+
channels&.find { |c| c.include?('rgba') }.nil? ? 'jpg' : 'png'
|
45
44
|
end
|
46
45
|
|
47
46
|
def unindexed_current_file_version
|
@@ -49,13 +48,18 @@ module Hyrax
|
|
49
48
|
ActiveFedora::File.uri_to_id(::FileSet.find(id).current_content_version_uri)
|
50
49
|
end
|
51
50
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
51
|
+
def latest_file_id
|
52
|
+
@latest_file_id ||=
|
53
|
+
begin
|
54
|
+
result = original_file_id
|
55
|
+
|
56
|
+
if result.blank?
|
57
|
+
Rails.logger.warn "original_file_id for #{id} not found, falling back to Fedora."
|
58
|
+
result = Hyrax::VersioningService.versioned_file_id ::FileSet.find(id).original_file
|
59
|
+
end
|
60
|
+
|
61
|
+
result
|
62
|
+
end
|
59
63
|
end
|
60
64
|
end
|
61
65
|
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hyrax
|
4
|
+
##
|
5
|
+
# This presenter wraps objects in the interface required by `IIIFManifiest`.
|
6
|
+
# It will accept either a Work-like resource or a SolrDocument.
|
7
|
+
#
|
8
|
+
# @example with a work
|
9
|
+
#
|
10
|
+
# monograph = Monograph.new
|
11
|
+
# presenter = IiifManifestPresenter.new(monograph)
|
12
|
+
# presenter.title # => []
|
13
|
+
#
|
14
|
+
# monograph.title = ['Comet in Moominland']
|
15
|
+
# presenter.title # => ['Comet in Moominland']
|
16
|
+
#
|
17
|
+
# @see https://www.rubydoc.info/gems/iiif_manifest
|
18
|
+
class IiifManifestPresenter < Draper::Decorator
|
19
|
+
delegate_all
|
20
|
+
|
21
|
+
##
|
22
|
+
# @!attribute [w] ability
|
23
|
+
# @return [Ability]
|
24
|
+
# @!attribute [w] hostname
|
25
|
+
# @return [String]
|
26
|
+
attr_writer :ability, :hostname
|
27
|
+
|
28
|
+
class << self
|
29
|
+
##
|
30
|
+
# @param [Hyrax::Resource, SolrDocument]
|
31
|
+
def for(model)
|
32
|
+
klass = model.file_set? ? DisplayImagePresenter : IiifManifestPresenter
|
33
|
+
|
34
|
+
klass.new(model)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# @return [#can?]
|
40
|
+
def ability
|
41
|
+
@ability ||= NullAbility.new
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# @return [String]
|
46
|
+
def description
|
47
|
+
Array(super).first || ''
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# @return [Boolean]
|
52
|
+
def file_set?
|
53
|
+
model.try(:file_set?) || Array(model[:has_model_ssim]).include?('FileSet')
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# @return [Array<DisplayImagePresenter>]
|
58
|
+
def file_set_presenters
|
59
|
+
member_presenters.select(&:file_set?)
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# IIIF metadata for inclusion in the manifest
|
64
|
+
# Called by the `iiif_manifest` gem to add metadata
|
65
|
+
#
|
66
|
+
# @todo should this use the simple_form i18n keys?! maybe the manifest
|
67
|
+
# needs its own?
|
68
|
+
#
|
69
|
+
# @return [Array<Hash{String => String}>] array of metadata hashes
|
70
|
+
def manifest_metadata
|
71
|
+
metadata_fields.map do |field_name|
|
72
|
+
{
|
73
|
+
'label' => I18n.t("simple_form.labels.defaults.#{field_name}"),
|
74
|
+
'value' => Array(self[field_name]).map { |value| scrub(value.to_s) }
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# @return [String] the URL where the manifest can be found
|
81
|
+
def manifest_url
|
82
|
+
return '' if id.blank?
|
83
|
+
|
84
|
+
Rails.application.routes.url_helpers.polymorphic_url([:manifest, model], host: hostname)
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# @return [Array<#to_s>]
|
89
|
+
def member_ids
|
90
|
+
Hyrax::SolrDocument::OrderedMembers.decorate(model).ordered_member_ids
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# @note cache member presenters to avoid querying repeatedly; we expect this
|
95
|
+
# presenter to live only as long as the request.
|
96
|
+
#
|
97
|
+
# @note skips presenters for objects the current `@ability` cannot read.
|
98
|
+
# the default ability has all permissions.
|
99
|
+
#
|
100
|
+
# @return [Array<IiifManifestPresenter>]
|
101
|
+
def member_presenters
|
102
|
+
@member_presenters_cache ||= Factory.build_for(ids: member_ids, presenter_class: self.class).map do |presenter|
|
103
|
+
next unless ability.can?(:read, presenter.model)
|
104
|
+
|
105
|
+
presenter.hostname = hostname
|
106
|
+
presenter.ability = ability
|
107
|
+
presenter
|
108
|
+
end.compact
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# @return [Array<Hash{String => String}>]
|
113
|
+
def sequence_rendering
|
114
|
+
Array(try(:rendering_ids)).map do |file_set_id|
|
115
|
+
rendering = file_set_presenters.find { |p| p.id == file_set_id }
|
116
|
+
next unless rendering
|
117
|
+
|
118
|
+
{ '@id' => Hyrax::Engine.routes.url_helpers.download_url(rendering.id, host: hostname),
|
119
|
+
'format' => rendering.mime_type.present? ? rendering.mime_type : I18n.t("hyrax.manifest.unknown_mime_text"),
|
120
|
+
'label' => I18n.t("hyrax.manifest.download_text") + (rendering.label || '') }
|
121
|
+
end.flatten
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# @return [Boolean]
|
126
|
+
def work?
|
127
|
+
object.try(:work?) || !file_set?
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# @return [Array<IiifManifestPresenter>]
|
132
|
+
def work_presenters
|
133
|
+
member_presenters.select(&:work?)
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# @note ideally, this value will be cheap to retrieve, and will reliably
|
138
|
+
# change any time the manifest JSON will change. the current implementation
|
139
|
+
# is more blunt than this, changing only when the work itself changes.
|
140
|
+
#
|
141
|
+
# @return [String] a string tag suitable for cache keys for this manifiest
|
142
|
+
def version
|
143
|
+
object.try(:modified_date)&.to_s || ''
|
144
|
+
end
|
145
|
+
|
146
|
+
##
|
147
|
+
# An Ability-like object that gives `true` for all `can?` requests
|
148
|
+
class NullAbility
|
149
|
+
##
|
150
|
+
# @return [Boolean] true
|
151
|
+
def can?(*)
|
152
|
+
true
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class Factory < PresenterFactory
|
157
|
+
##
|
158
|
+
# @return [Array]
|
159
|
+
def build
|
160
|
+
ids.map do |id|
|
161
|
+
solr_doc = load_docs.find { |doc| doc.id == id }
|
162
|
+
presenter_class.for(solr_doc) if solr_doc
|
163
|
+
end.compact
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
##
|
169
|
+
# cache the docs in this method, rather than #build;
|
170
|
+
# this can probably be pushed up to the parent class
|
171
|
+
def load_docs
|
172
|
+
@cached_docs ||= super
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
##
|
177
|
+
# a Presenter for producing `IIIFManifest::DisplayImage` objects
|
178
|
+
#
|
179
|
+
class DisplayImagePresenter < Draper::Decorator
|
180
|
+
delegate_all
|
181
|
+
|
182
|
+
include Hyrax::DisplaysImage
|
183
|
+
|
184
|
+
##
|
185
|
+
# @!attribute [w] ability
|
186
|
+
# @return [Ability]
|
187
|
+
# @!attribute [w] hostname
|
188
|
+
# @return [String]
|
189
|
+
attr_writer :ability, :hostname
|
190
|
+
|
191
|
+
##
|
192
|
+
# Creates a display image only where #model is an image.
|
193
|
+
#
|
194
|
+
# @return [IIIFManifest::DisplayImage] the display image required by the manifest builder.
|
195
|
+
def display_image
|
196
|
+
return nil unless model.image?
|
197
|
+
return nil unless latest_file_id
|
198
|
+
|
199
|
+
IIIFManifest::DisplayImage
|
200
|
+
.new(display_image_url(hostname),
|
201
|
+
format: image_format(alpha_channels),
|
202
|
+
width: width,
|
203
|
+
height: height,
|
204
|
+
iiif_endpoint: iiif_endpoint(latest_file_id, base_url: hostname))
|
205
|
+
end
|
206
|
+
|
207
|
+
def hostname
|
208
|
+
@hostname || 'localhost'
|
209
|
+
end
|
210
|
+
|
211
|
+
##
|
212
|
+
# @return [Boolean] false
|
213
|
+
def work?
|
214
|
+
false
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
def hostname
|
221
|
+
@hostname || 'localhost'
|
222
|
+
end
|
223
|
+
|
224
|
+
def metadata_fields
|
225
|
+
Hyrax.config.iiif_metadata_fields
|
226
|
+
end
|
227
|
+
|
228
|
+
def scrub(value)
|
229
|
+
Loofah.fragment(value).scrub!(:whitewash).to_s
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -36,14 +36,8 @@ module Hyrax
|
|
36
36
|
@work_presenters ||= member_presenters(ordered_ids - file_set_ids, work_presenter_class)
|
37
37
|
end
|
38
38
|
|
39
|
-
# TODO: Extract this to ActiveFedora::Aggregations::ListSource
|
40
39
|
def ordered_ids
|
41
|
-
@ordered_ids ||=
|
42
|
-
ActiveFedora::SolrService.query("proxy_in_ssi:#{id}",
|
43
|
-
rows: 10_000,
|
44
|
-
fl: "ordered_targets_ssim")
|
45
|
-
.flat_map { |x| x.fetch("ordered_targets_ssim", []) }
|
46
|
-
end
|
40
|
+
@ordered_ids ||= Hyrax::SolrDocument::OrderedMembers.decorate(@work).ordered_member_ids
|
47
41
|
end
|
48
42
|
|
49
43
|
private
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hyrax
|
4
|
+
##
|
5
|
+
# constructs IIIF Manifests and holds them in the Rails cache,
|
6
|
+
# this approach avoids long manifest build times for some kinds of requests,
|
7
|
+
# at the cost of introducing cache invalidation issues.
|
8
|
+
class CachingIiifManifestBuilder < ManifestBuilderService
|
9
|
+
KEY_PREFIX = 'iiif-cache-v1'
|
10
|
+
|
11
|
+
attr_accessor :expires_in
|
12
|
+
|
13
|
+
##
|
14
|
+
# @api public
|
15
|
+
#
|
16
|
+
# @param iiif_manifest_factory [Class] a class that initializes with presenter
|
17
|
+
# object and returns an object that responds to `#to_h`
|
18
|
+
# @param expires_in [Integer] the number of seconds until the cache expires
|
19
|
+
# @see Hyrax::Configuration#iiif_manifest_cache_duration
|
20
|
+
def initialize(iiif_manifest_factory: ::IIIFManifest::ManifestFactory, expires_in: Hyrax.config.iiif_manifest_cache_duration)
|
21
|
+
self.expires_in = expires_in
|
22
|
+
|
23
|
+
super(iiif_manifest_factory: iiif_manifest_factory)
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# @see ManifestBuilderService#as_json
|
28
|
+
def manifest_for(presenter:)
|
29
|
+
Rails.cache.fetch(manifest_cache_key(presenter: presenter), expires_in: expires_in) do
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
##
|
37
|
+
# @note adding a version_for suffix helps us manage cache expiration,
|
38
|
+
# reducing false cache hits
|
39
|
+
#
|
40
|
+
# @param presenter [Hyrax::IiifManifestPresenter]
|
41
|
+
#
|
42
|
+
# @return [String]
|
43
|
+
def manifest_cache_key(presenter:)
|
44
|
+
"#{KEY_PREFIX}_#{presenter.id}/#{version_for(presenter)}"
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# @return [String]
|
49
|
+
def version_for(presenter)
|
50
|
+
presenter.version
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -13,7 +13,7 @@ module Hyrax
|
|
13
13
|
# If calling from Abilities, pass the ability. If you try to get the ability from the user, you end up in an infinit loop.
|
14
14
|
def self.collection_type_ids_for_user(roles:, user: nil, ability: nil)
|
15
15
|
return false unless user.present? || ability.present?
|
16
|
-
return Hyrax::CollectionType.all.pluck('DISTINCT id') if user_admin?(user, ability)
|
16
|
+
return Hyrax::CollectionType.all.pluck(Arel.sql('DISTINCT id')) if user_admin?(user, ability)
|
17
17
|
Hyrax::CollectionTypeParticipant.where(agent_type: Hyrax::CollectionTypeParticipant::USER_TYPE,
|
18
18
|
agent_id: user_id(user, ability),
|
19
19
|
access: roles)
|
@@ -21,7 +21,7 @@ module Hyrax
|
|
21
21
|
Hyrax::CollectionTypeParticipant.where(agent_type: Hyrax::CollectionTypeParticipant::GROUP_TYPE,
|
22
22
|
agent_id: user_groups(user, ability),
|
23
23
|
access: roles)
|
24
|
-
).pluck('DISTINCT hyrax_collection_type_id')
|
24
|
+
).pluck(Arel.sql('DISTINCT hyrax_collection_type_id'))
|
25
25
|
end
|
26
26
|
|
27
27
|
# @api public
|
@@ -174,7 +174,7 @@ module Hyrax
|
|
174
174
|
def self.agent_ids_for(collection_type:, agent_type:, access:)
|
175
175
|
Hyrax::CollectionTypeParticipant.where(hyrax_collection_type_id: collection_type.id,
|
176
176
|
agent_type: agent_type,
|
177
|
-
access: access).pluck('DISTINCT agent_id')
|
177
|
+
access: access).pluck(Arel.sql('DISTINCT agent_id'))
|
178
178
|
end
|
179
179
|
private_class_method :agent_ids_for
|
180
180
|
|
@@ -13,7 +13,7 @@ module Hyrax
|
|
13
13
|
def self.source_ids_for_user(access:, ability:, source_type: nil, exclude_groups: [])
|
14
14
|
scope = PermissionTemplateAccess.for_user(ability: ability, access: access, exclude_groups: exclude_groups)
|
15
15
|
.joins(:permission_template)
|
16
|
-
ids = scope.pluck('DISTINCT source_id')
|
16
|
+
ids = scope.pluck(Arel.sql('DISTINCT source_id'))
|
17
17
|
return ids unless source_type
|
18
18
|
filter_source(source_type: source_type, ids: ids)
|
19
19
|
end
|
@@ -10,7 +10,7 @@ module Hyrax
|
|
10
10
|
|
11
11
|
def show
|
12
12
|
if parent_presenter
|
13
|
-
polymorphic_path([:hyrax, :parent, presenter.model_name.singular],
|
13
|
+
polymorphic_path([:hyrax, :parent, presenter.model_name.singular.to_sym],
|
14
14
|
parent_id: parent_presenter.id,
|
15
15
|
id: presenter.id)
|
16
16
|
else
|