Class: AtlasRb::Work
Overview
The bibliographic unit in Atlas — an article, thesis, dataset, image, etc.
A Work belongs to exactly one Collection and aggregates one or more FileSets, each of which holds binary content via a Blob. MODS metadata is attached at the Work level.
See also: Collection, FileSet, Blob.
Constant Summary collapse
- ROUTE =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
Atlas REST endpoint prefix for this resource.
"/works/"- ASSOCIATION_TYPES =
The five relationship predicates Atlas accepts, carried here so a caller can build a select box without hard-coding the vocabulary. Adding a sixth needs an Atlas release, so this list cannot drift ahead of the server.
%w[ is_codebook_for is_figure_for is_instructional_material_for is_supplemental_material_for is_transcription_of ].freeze
Constants inherited from Resource
Constants included from FaradayHelper
FaradayHelper::ASSERTION_AUDIENCE, FaradayHelper::ASSERTION_ISSUER, FaradayHelper::ASSERTION_TTL, FaradayHelper::INSTRUMENTATION_EVENT
Class Method Summary collapse
-
.add_linked_member(work_id, collection_id, nuid: nil, on_behalf_of: nil) ⇒ Array<String>
Add a linked membership: surface a Work in an additional Collection.
- .assets(id, nuid: nil, on_behalf_of: nil) ⇒ Array<AtlasRb::Mash>?
-
.associate(work_id, target_id, type:, nuid: nil, on_behalf_of: nil) ⇒ Hash
Assert that this Work stands in a typed relationship to another Work.
-
.associations(id, nuid: nil, on_behalf_of: nil) ⇒ Hash?
List a Work's typed associations with other Works.
-
.clear_incomplete(id, nuid: nil, on_behalf_of: nil) ⇒ Hash
Clear the incomplete flag and its reason.
-
.complete(id, nuid: nil, on_behalf_of: nil) ⇒ Faraday::Response
Mark a Work complete.
-
.create(id, xml_path = nil, idempotency_key: nil, nuid: nil, on_behalf_of: nil, depositor: nil) ⇒ Hash
Create a new Work in an existing Collection.
-
.disassociate(work_id, target_id, type:, nuid: nil, on_behalf_of: nil) ⇒ Hash
Retract one typed relationship between two Works.
-
.file_sets(id, nuid: nil, on_behalf_of: nil) ⇒ Array<AtlasRb::Mash>?
List a Work's page FileSets in order, each with its assets.
-
.find(id, nuid: nil, on_behalf_of: nil) ⇒ Hash?
Fetch a single Work by ID.
-
.linked_members(id, nuid: nil, on_behalf_of: nil) ⇒ Array<String>?
List the Collections a Work is a linked member of.
-
.list(in_progress: nil, incomplete: nil, page: nil, per_page: nil, nuid: nil, on_behalf_of: nil) ⇒ AtlasRb::Mash?
List Works, paginated.
-
.mark_incomplete(id, reason:, nuid: nil, on_behalf_of: nil) ⇒ Hash
Flag a Work whose enrichment pipeline gave up.
-
.mets(id, nuid: nil, on_behalf_of: nil) ⇒ Hash?
Fetch the Work-level METS structural metadata (page order).
-
.mods(id, kind = nil, nuid: nil, on_behalf_of: nil) ⇒ String?
Fetch the Work's MODS representation in the requested format.
-
.remove_linked_member(work_id, collection_id, nuid: nil, on_behalf_of: nil) ⇒ Array<String>
Remove a linked membership: drop a Work from an additional Collection.
-
.set_derivative_permissions(id, policy:, nuid: nil, on_behalf_of: nil) ⇒ AtlasRb::Mash
Replace a Work's per-asset derivative-visibility policy.
-
.set_full_text(id, text:, nuid: nil, on_behalf_of: nil) ⇒ AtlasRb::Mash
Store a Work's derived full-document text for search indexing.
-
.set_image_derivatives(id, small: nil, medium: nil, large: nil, nuid: nil, on_behalf_of: nil) ⇒ AtlasRb::Mash
Attach the three image-derivative Delegate URIs to a Work.
Methods inherited from Resource
class_for, descendant_works, find_many, history, mods_version, mods_versions, permissions, preview, put_mods, reparent, set_permissions, set_thumbnails, tombstone
Methods included from FaradayHelper
#connection, #multipart, #read_body, #read_raw, #system_connection, #with_file_part
Class Method Details
.add_linked_member(work_id, collection_id, nuid: nil, on_behalf_of: nil) ⇒ Array<String>
Add a linked membership: surface a Work in an additional Collection.
Wraps POST /works/<id>/linked_members with a collection_id body.
This does not move the Work — its structural parent (a_member_of)
is untouched; the Collection is added to a_linked_member_of. Atlas
enforces two-sided authorization (edit on the Work and the target
Collection) and the structural guards, surfacing failures as a 422.
Permissions are never changed by this call.
635 636 637 638 639 640 |
# File 'lib/atlas_rb/work.rb', line 635 def self.add_linked_member(work_id, collection_id, nuid: nil, on_behalf_of: nil) write_resource( connection({ collection_id: collection_id }, nuid, on_behalf_of: on_behalf_of) .post(ROUTE + work_id + '/linked_members') ) end |
.assets(id, nuid: nil, on_behalf_of: nil) ⇒ Array<AtlasRb::Mash>?
List the assets attached to a Work — Blobs and Delegates alike.
Useful for building download UIs — the response includes enough to
render each entry's display name, size or uri, and download URL.
The shape is polymorphic: Blob-backed entries carry fields like
size, while Delegate-backed entries carry uri. Callers should
duck-type on the field they need rather than expecting a single
schema.
Every entry (Blob and Delegate) also carries the advisory read gate set
via set_derivative_permissions: gated (true if the asset must be
authorized rather than fetched directly) and permission (the effective
read-group set, or nil for guests, to whom group names are withheld).
455 456 457 458 459 |
# File 'lib/atlas_rb/work.rb', line 455 def self.assets(id, nuid: nil, on_behalf_of: nil) read_body(connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/assets')) do |body| body.map { |entry| AtlasRb::Mash.new(entry) } end end |
.associate(work_id, target_id, type:, nuid: nil, on_behalf_of: nil) ⇒ Hash
Assert that this Work stands in a typed relationship to another Work.
Wraps POST /works/<id>/associations with a work_id + type body.
The edge is stored on this Work only; target reports the same edge
under inbound. Asserting an edge that already exists is a no-op, and
two Works can hold several different edges at once.
A cycle is permitted and meaningful — "A is a transcription of B" and "B is a figure for A" can both be true.
766 767 768 769 770 771 |
# File 'lib/atlas_rb/work.rb', line 766 def self.associate(work_id, target_id, type:, nuid: nil, on_behalf_of: nil) write_resource( connection({ work_id: target_id, type: type }, nuid, on_behalf_of: on_behalf_of) .post(ROUTE + work_id + '/associations') ) end |
.associations(id, nuid: nil, on_behalf_of: nil) ⇒ Hash?
List a Work's typed associations with other Works.
Wraps GET /works/<id>/associations. An association is DRS v1's
"associated works": a directed claim that one object is the codebook,
figure, transcription, instructional material or supplemental material
for another. Both objects stay separate records — this is not
membership, and nothing moves in the containment tree.
The edge is stored once, on the Work that asserts it. Atlas derives the
other direction, so outbound and inbound can never disagree.
726 727 728 |
# File 'lib/atlas_rb/work.rb', line 726 def self.associations(id, nuid: nil, on_behalf_of: nil) read_body(connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/associations')) end |
.clear_incomplete(id, nuid: nil, on_behalf_of: nil) ⇒ Hash
Clear the incomplete flag and its reason.
The repair half of mark_incomplete: call it from the same job when a later run succeeds, or by hand once an operator has fixed the Work. Both fields clear together — a reason without a flag would leave a stale cause on the Solr document.
Idempotent: clearing a Work that was never flagged is a no-op.
277 278 279 280 281 282 |
# File 'lib/atlas_rb/work.rb', line 277 def self.clear_incomplete(id, nuid: nil, on_behalf_of: nil) AtlasRb::Mash.new(write_resource( connection({}, nuid, on_behalf_of: on_behalf_of) .delete(ROUTE + id + '/incomplete') ))["work"] end |
.complete(id, nuid: nil, on_behalf_of: nil) ⇒ Faraday::Response
Mark a Work complete.
Cerberus's bulk-deposit job calls this once it has confirmed all
expected children (FileSets / Blobs) are deposited. Atlas's monitoring
query GET /works?in_progress=true then drops this Work from the
"stuck" list.
Idempotent on the server: calling complete on an already-complete
Work is a no-op — Atlas re-saves with in_progress: false.
Atlas does not currently stamp a completed_by audit field; the
nuid: parameter is plumbed through for parity with the other
lifecycle bindings and in case Atlas adds completion audit later.
This call also mints the Work's persistent identifier. Atlas
registers <prefix>/<noid> with its Handle service, pointed at the
public Work page, and records it as handle on the Work. Two
consequences for a caller:
- Minting can never fail the call. A handle server that is down,
slow or unconfigured leaves
handlenull and the Work still complete — never a non-2xx. So a200does not promise a handle: the response body carries the Work, so checkhandleon it rather than assuming success minted one. - Re-completing is safe. Atlas mints only when
handleis empty, and the underlying registration is keyed by handle name, so a repeat call re-points rather than minting a second identifier.
A deployment with no handle server configured mints nothing at all, which is the normal state for a stack brought up without it.
198 199 200 |
# File 'lib/atlas_rb/work.rb', line 198 def self.complete(id, nuid: nil, on_behalf_of: nil) connection({}, nuid, on_behalf_of: on_behalf_of).post(ROUTE + id + '/complete') end |
.create(id, xml_path = nil, idempotency_key: nil, nuid: nil, on_behalf_of: nil, depositor: nil) ⇒ Hash
Create a new Work in an existing Collection.
Note: unlike Community.create and Collection.create, the id
parameter here is the parent Collection ID. The underlying request
uses the collection_id query param rather than parent_id.
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/atlas_rb/work.rb', line 140 def self.create(id, xml_path = nil, idempotency_key: nil, nuid: nil, on_behalf_of: nil, depositor: nil) params = { collection_id: id } params[:depositor] = depositor if depositor result = AtlasRb::Mash.new(write_resource( connection(params, nuid, on_behalf_of: on_behalf_of, idempotency_key: idempotency_key).post(ROUTE) ))["work"] return result if xml_path.to_s.empty? # The MODS seed is a second call: create takes the parent and the # provenance slots, and the document goes through the write that owns it. AtlasRb::Resource.put_mods(result["id"], xml_path, nuid: nuid, on_behalf_of: on_behalf_of) find(result["id"], nuid: nuid, on_behalf_of: on_behalf_of) end |
.disassociate(work_id, target_id, type:, nuid: nil, on_behalf_of: nil) ⇒ Hash
Retract one typed relationship between two Works.
Wraps DELETE /works/<id>/associations/<type>/<target_id> — the type is
a path segment because it is part of the edge's identity, so retracting
the figure claim leaves a transcription claim between the same two Works
standing. Idempotent: retracting an edge that was never asserted is a
no-op.
803 804 805 806 807 808 |
# File 'lib/atlas_rb/work.rb', line 803 def self.disassociate(work_id, target_id, type:, nuid: nil, on_behalf_of: nil) write_resource( connection({}, nuid, on_behalf_of: on_behalf_of) .delete(ROUTE + work_id + '/associations/' + type.to_s + '/' + target_id) ) end |
.file_sets(id, nuid: nil, on_behalf_of: nil) ⇒ Array<AtlasRb::Mash>?
List a Work's page FileSets in order, each with its assets.
Wraps GET /works/<id>/file_sets — the ordered, grouped sibling of
assets (which flattens FileSet membership away). One entry per
page-bearing FileSet, sorted position ascending with unordered
(null-position) FileSets last; metadata and derivative-container
FileSets are excluded as entries. Each entry nests its downloadable
assets — the page's content Blobs plus any per-page IIIF Delegates —
in the same polymorphic shape assets returns.
This is the read a IIIF Presentation manifest assembler needs: the response is unpaginated by design, so the whole page sequence arrives in one call.
495 496 497 498 499 |
# File 'lib/atlas_rb/work.rb', line 495 def self.file_sets(id, nuid: nil, on_behalf_of: nil) read_body(connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/file_sets')) do |body| body.map { |entry| AtlasRb::Mash.new(entry) } end end |
.find(id, nuid: nil, on_behalf_of: nil) ⇒ Hash?
Fetch a single Work by ID.
33 34 35 36 |
# File 'lib/atlas_rb/work.rb', line 33 def self.find(id, nuid: nil, on_behalf_of: nil) body = fetch_resource(ROUTE + id, nuid: nuid, on_behalf_of: on_behalf_of) body && AtlasRb::Mash.new(body)["work"] end |
.linked_members(id, nuid: nil, on_behalf_of: nil) ⇒ Array<String>?
List the Collections a Work is a linked member of.
Wraps GET /works/<id>/linked_members. Linked membership is the DAG
overlay — a Work has exactly one structural parent (a_member_of, set
by create / Resource.reparent) but may additionally appear in any number
of other Collections as a linked member (a_linked_member_of). This
returns just those linked Collection noids; the structural parent is
not included.
595 596 597 |
# File 'lib/atlas_rb/work.rb', line 595 def self.linked_members(id, nuid: nil, on_behalf_of: nil) read_body(connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/linked_members')) end |
.list(in_progress: nil, incomplete: nil, page: nil, per_page: nil, nuid: nil, on_behalf_of: nil) ⇒ AtlasRb::Mash?
List Works, paginated.
Wraps GET /works. Returns the full pagination envelope — works plus
pagination — rather than a bare array, so callers can page through
results.
79 80 81 82 83 84 85 86 87 88 |
# File 'lib/atlas_rb/work.rb', line 79 def self.list(in_progress: nil, incomplete: nil, page: nil, per_page: nil, nuid: nil, on_behalf_of: nil) params = {} params[:in_progress] = in_progress unless in_progress.nil? params[:incomplete] = incomplete unless incomplete.nil? params[:page] = page if page params[:per_page] = per_page if per_page read_body(connection(params, nuid, on_behalf_of: on_behalf_of).get(ROUTE)) do |body| AtlasRb::Mash.new(body) end end |
.mark_incomplete(id, reason:, nuid: nil, on_behalf_of: nil) ⇒ Hash
Flag a Work whose enrichment pipeline gave up.
The counterpart to complete, for the other half of the lifecycle:
complete says the deposit finished, this says something downstream of
it did not. Call it from a work-scoped job's give-up handler — the PDF or
media rendition, the derivatives, the full-text extraction — once that
job has exhausted its retries. Atlas's GET /works?incomplete=true then
lists the Work for staff, and incomplete_bsi on its Solr document lets
a result row render a pill without a per-row fetch.
The flag never hides the Work. A record with its file, title and metadata but one missing derivative is degraded, not broken, and stays readable — enrichment does not fail a deposit.
Idempotent on the server; the last reason wins. Clear it with clear_incomplete when a later run of the same job succeeds, which makes the state self-healing.
244 245 246 247 248 249 |
# File 'lib/atlas_rb/work.rb', line 244 def self.mark_incomplete(id, reason:, nuid: nil, on_behalf_of: nil) AtlasRb::Mash.new(write_resource( connection({}, nuid, on_behalf_of: on_behalf_of) .post(ROUTE + id + '/incomplete', JSON.dump(reason: reason)) ))["work"] end |
.mets(id, nuid: nil, on_behalf_of: nil) ⇒ Hash?
Fetch the Work-level METS structural metadata (page order).
Wraps GET /works/<id>/mets — the JSON projection of the Work's METS
document, whose physical structMap is the preservation record of page
order. The page sequence surfaces under "mets" => "pages" (one entry
per page: noid / order / label). Atlas builds the document when
the Work is completed (complete) and rebuilds it on page changes
thereafter, so a Work that has never been completed has no METS yet —
Atlas answers 404 and this binding returns nil (matching
User.find_by_nuid's missing-resource convention).
For runtime page listing (e.g. manifest assembly) prefer file_sets, which needs no completion and carries each page's assets; this read is the preservation-record view.
534 535 536 537 538 |
# File 'lib/atlas_rb/work.rb', line 534 def self.mets(id, nuid: nil, on_behalf_of: nil) read_body(connection({}, nuid, on_behalf_of: on_behalf_of).get(ROUTE + id + '/mets')) do |body| AtlasRb::Mash.new(body)["work"] end end |
.mods(id, kind = nil, nuid: nil, on_behalf_of: nil) ⇒ String?
Fetch the Work's MODS representation in the requested format.
560 561 562 563 564 565 |
# File 'lib/atlas_rb/work.rb', line 560 def self.mods(id, kind = nil, nuid: nil, on_behalf_of: nil) # json default, html, xml read_raw(connection({}, nuid, on_behalf_of: on_behalf_of).get( ROUTE + id + '/mods' + (kind.to_s.empty? ? '' : ".#{kind}") )) end |
.remove_linked_member(work_id, collection_id, nuid: nil, on_behalf_of: nil) ⇒ Array<String>
Remove a linked membership: drop a Work from an additional Collection.
Wraps DELETE /works/<id>/linked_members/<collection_id> — the
Collection is passed as a path segment, not a body. This removes the
Collection from the Work's a_linked_member_of; the structural parent
(a_member_of) is untouched. Atlas enforces the same two-sided
authorization as add_linked_member. Removing a link that does not
exist is a server-side concern; this binding simply forwards the call.
677 678 679 680 681 682 |
# File 'lib/atlas_rb/work.rb', line 677 def self.remove_linked_member(work_id, collection_id, nuid: nil, on_behalf_of: nil) write_resource( connection({}, nuid, on_behalf_of: on_behalf_of) .delete(ROUTE + work_id + '/linked_members/' + collection_id) ) end |
.set_derivative_permissions(id, policy:, nuid: nil, on_behalf_of: nil) ⇒ AtlasRb::Mash
Replace a Work's per-asset derivative-visibility policy.
Sets which read groups may fetch each of the Work's downloadable
renditions, reusing the resource read-group vocabulary ("public",
Grouper group names, [] = private). Two media families:
- the image ladder
small/medium/large/service(deep-zoom) /master(the original image), and - independent media
audio/video/pdf.
Unlike set_image_derivatives (which upserts URIs) this is a whole-object
REPLACE: the map you pass is the complete policy. Within the image ladder
omitted tiers inherit by cascade (an absent tier inherits the next
lower-resolution tier; small inherits the Work's own visibility).
Independent media do NOT cascade — an absent audio/video/pdf key
rides the Work. Pass a tier as [] to make it private.
Atlas enforces: a tier may not be more visible than the Work, and — within
the image ladder — visibility must narrow as resolution grows
(master ⊆ service ⊆ large ⊆ medium ⊆ small; independent media
impose no ordering). The gate is advisory — it surfaces on assets as
gated / permission for BOTH Delegate (image tier) and Blob (master /
pdf / audio / video, classified by media type) entries, for the display
layer (Cerberus / the IIIF auth service; Cerberus's download :read check)
to enforce.
377 378 379 380 381 382 |
# File 'lib/atlas_rb/work.rb', line 377 def self.(id, policy:, nuid: nil, on_behalf_of: nil) AtlasRb::Mash.new(write_resource( connection({}, nuid, on_behalf_of: on_behalf_of) .patch(ROUTE + id + '/derivative_permissions', JSON.dump(policy)) )) end |
.set_full_text(id, text:, nuid: nil, on_behalf_of: nil) ⇒ AtlasRb::Mash
Store a Work's derived full-document text for search indexing.
Purpose-specific PATCH in the same "machine-set derived metadata" family
as Resource.set_thumbnails / set_image_derivatives. Hand Atlas the Work-level
aggregate of the extracted body text (the concatenation of the Work's
content FileSets' text); Atlas stores it as the Work's derived full_text
and its FullTextIndexer projects it onto the Work's Solr doc
(all_text_timv) for body-text search + the "Full Text Match" snippet.
Distinct from metadata — this is a machine-extracted search aid (pdftotext / Tika in a Cerberus job), not user-authored descriptive content, and is re-sent on any re-ingest. Empty/blank text clears it.
417 418 419 420 421 422 |
# File 'lib/atlas_rb/work.rb', line 417 def self.set_full_text(id, text:, nuid: nil, on_behalf_of: nil) AtlasRb::Mash.new(write_resource( connection({}, nuid, on_behalf_of: on_behalf_of) .patch(ROUTE + id + '/full_text', JSON.dump(text: text)) )) end |
.set_image_derivatives(id, small: nil, medium: nil, large: nil, nuid: nil, on_behalf_of: nil) ⇒ AtlasRb::Mash
Attach the three image-derivative Delegate URIs to a Work.
Sibling of Resource.set_thumbnails for the small_image /
medium_image / large_image Delegate roles. Atlas dispatches
each URI to its matching role via DelegateUpdater. The
resulting Delegates are downloadable and surface through
assets for the downloads UI. Missing keys are left untouched
server-side; only the URIs you pass are upserted.
316 317 318 319 320 321 322 |
# File 'lib/atlas_rb/work.rb', line 316 def self.set_image_derivatives(id, small: nil, medium: nil, large: nil, nuid: nil, on_behalf_of: nil) body = { small: small, medium: medium, large: large }.compact AtlasRb::Mash.new(write_resource( connection({}, nuid, on_behalf_of: on_behalf_of) .patch(ROUTE + id + '/image_derivatives', JSON.dump(body)) )) end |