hyrax 2.8.0 → 2.9.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|