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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +8 -8
  3. data/.gitignore +1 -0
  4. data/.regen +1 -1
  5. data/README.md +1 -1
  6. data/app/assets/javascripts/hyrax.js +1 -0
  7. data/app/assets/javascripts/hyrax/autocomplete.es6 +29 -0
  8. data/app/assets/javascripts/hyrax/editor.es6 +9 -10
  9. data/app/assets/javascripts/hyrax/skip_to_content.js +15 -0
  10. data/app/controllers/concerns/hyrax/works_controller_behavior.rb +18 -7
  11. data/app/controllers/hyrax/file_sets_controller.rb +6 -1
  12. data/app/controllers/hyrax/users_controller.rb +1 -1
  13. data/app/helpers/hyrax/hyrax_helper_behavior.rb +1 -0
  14. data/app/helpers/hyrax/work_form_helper.rb +48 -0
  15. data/app/jobs/embargo_expiry_job.rb +15 -0
  16. data/app/jobs/iiif_manifest_cache_prewarm_job.rb +16 -0
  17. data/app/jobs/lease_expiry_job.rb +15 -0
  18. data/app/models/concerns/hyrax/ability.rb +1 -1
  19. data/app/models/concerns/hyrax/solr_document/characterization.rb +1 -1
  20. data/app/models/concerns/hyrax/solr_document/metadata.rb +1 -0
  21. data/app/models/concerns/hyrax/solr_document/ordered_members.rb +46 -0
  22. data/app/models/concerns/hyrax/solr_document_behavior.rb +10 -0
  23. data/app/presenters/hyrax/displays_image.rb +25 -21
  24. data/app/presenters/hyrax/iiif_manifest_presenter.rb +232 -0
  25. data/app/presenters/hyrax/member_presenter_factory.rb +1 -7
  26. data/app/services/hyrax/caching_iiif_manifest_builder.rb +53 -0
  27. data/app/services/hyrax/collection_types/permissions_service.rb +3 -3
  28. data/app/services/hyrax/collections/permissions_service.rb +1 -1
  29. data/app/services/hyrax/contextual_path.rb +1 -1
  30. data/app/services/hyrax/identifier/builder.rb +45 -0
  31. data/app/services/hyrax/identifier/dispatcher.rb +61 -0
  32. data/app/services/hyrax/identifier/registrar.rb +41 -0
  33. data/app/services/hyrax/manifest_builder_service.rb +88 -0
  34. data/app/services/hyrax/versioning_service.rb +9 -0
  35. data/app/views/hyrax/base/_form.html.erb +1 -1
  36. data/app/views/hyrax/base/_form_child_work_relationships.html.erb +1 -1
  37. data/app/views/hyrax/base/_form_progress.html.erb +4 -0
  38. data/app/views/hyrax/base/_form_visibility_error.html.erb +2 -0
  39. data/app/views/hyrax/base/_guts4form.html.erb +7 -1
  40. data/app/views/hyrax/base/_show_actions.html.erb +1 -1
  41. data/app/views/hyrax/batch_uploads/_form.html.erb +2 -2
  42. data/app/views/hyrax/dashboard/_sidebar.html.erb +1 -1
  43. data/config/features.rb +4 -0
  44. data/hyrax.gemspec +3 -2
  45. data/lib/generators/hyrax/templates/catalog_controller.rb +4 -0
  46. data/lib/generators/hyrax/templates/config/initializers/hyrax.rb +5 -0
  47. data/lib/generators/hyrax/templates/config/locales/hyrax.es.yml +1 -1
  48. data/lib/hyrax.rb +1 -0
  49. data/lib/hyrax/configuration.rb +23 -4
  50. data/lib/hyrax/engine.rb +1 -0
  51. data/lib/hyrax/specs/shared_specs.rb +1 -0
  52. data/lib/hyrax/specs/shared_specs/identifiers.rb +27 -0
  53. data/lib/hyrax/version.rb +1 -1
  54. data/template.rb +1 -1
  55. 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(url,
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 iiif_endpoint(file_id)
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, request.base_url),
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.find { |c| c.include?('rgba') }.nil? ? 'jpg' : 'png'
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 lookup_original_file_id
53
- result = original_file_id
54
- if result.blank?
55
- Rails.logger.warn "original_file_id for #{id} not found, falling back to Fedora."
56
- result = ActiveFedora::File.uri_to_id(::FileSet.find(id).current_content_version_uri)
57
- end
58
- result
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 ||= begin
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