hyrax 2.8.0 → 2.9.5

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