From 1b4302eab77c99385fb7fe0c80ffd35cf72bef9e Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Thu, 7 May 2026 13:58:10 -0400 Subject: [PATCH 01/38] bundle exec rails g hyrax:listeners --- app/listeners/hyrax_listener.rb | 81 ++++++++++++++++++++++++++++++++ config/initializers/publisher.rb | 3 ++ 2 files changed, 84 insertions(+) create mode 100644 app/listeners/hyrax_listener.rb create mode 100644 config/initializers/publisher.rb diff --git a/app/listeners/hyrax_listener.rb b/app/listeners/hyrax_listener.rb new file mode 100644 index 000000000..058bbefa0 --- /dev/null +++ b/app/listeners/hyrax_listener.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +## +# Generated by hyrax:listeners +# +# The Hyrax engine uses a publish/subscribe programming model to allow +# pluggable behavior in response to certain repository events. A range of events +# are published on a topic based event bus. +# +# This listener provides a template. +# +# For simple use cases, it's fine to add behavior to the `#on_*` methods in this +# Listener. If you have more than trivial behavior here, you probably want to add +# new classes that are named narrowly scoped and named for what the listener is +# for. +# +# When writing listener methods, it's important to carefully consider error, +# handling. Unhandled exceptions short-circuit behavior for other listeners, +# so it's a good idea to be paying attention to failure cases. +# +# @see https://github.com/samvera/hyrax/wiki/Hyrax's-Event-Bus-(Hyrax::Publisher) +# @see https://www.rubydoc.info/gems/hyrax/Hyrax/Publisher +# @see https://dry-rb.org/gems/dry-events +class HyraxListener + # def on_batch_created + # end + + # def on_collection_deleted + # end + + # def on_collection_metadata_updated + # end + + # def on_collection_membership_update + # end + + # def on_file_characterized + # end + + # def on_file_downloaded + # end + + # def on_file_metadata_updated + # end + + # def on_file_metadata_deleted + # end + + # def on_file_uploaded + # end + + # def on_file_set_audited + # end + + # def on_file_set_attached + # end + + # def on_file_set_url_imported + # end + + # def on_file_set_restored + # end + + # def on_object_deleted + # end + + # def on_object_failed_deposit + # end + + # def on_object_deposited + # end + + # def on_object_acl_updated + # end + + # def on_object_membership_updated + # end + + # def on_object_metadata_updated + # end +end diff --git a/config/initializers/publisher.rb b/config/initializers/publisher.rb new file mode 100644 index 000000000..828104b99 --- /dev/null +++ b/config/initializers/publisher.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +# Hyrax.publisher.subscribe(HyraxListener.new) From 8509440d8d2338475497e7c5365d4ffe06d6ab55 Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Thu, 7 May 2026 15:07:15 -0400 Subject: [PATCH 02/38] install valkyrie migrations --- ...1_enable_uuid_extension.valkyrie_engine.rb | 7 ++ ...42_create_orm_resources.valkyrie_engine.rb | 19 +++++ ...l_type_to_orm_resources.valkyrie_engine.rb | 7 ++ ..._type_to_internal_model.valkyrie_engine.rb | 7 ++ ...5_create_path_gin_index.valkyrie_engine.rb | 7 ++ ...internal_resource_index.valkyrie_engine.rb | 7 ++ ...create_updated_at_index.valkyrie_engine.rb | 7 ++ ...ocking_to_orm_resources.valkyrie_engine.rb | 7 ++ db/structure.sql | 82 +++++++++++++++++-- 9 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20260507181541_enable_uuid_extension.valkyrie_engine.rb create mode 100644 db/migrate/20260507181542_create_orm_resources.valkyrie_engine.rb create mode 100644 db/migrate/20260507181543_add_model_type_to_orm_resources.valkyrie_engine.rb create mode 100644 db/migrate/20260507181544_change_model_type_to_internal_model.valkyrie_engine.rb create mode 100644 db/migrate/20260507181545_create_path_gin_index.valkyrie_engine.rb create mode 100644 db/migrate/20260507181546_create_internal_resource_index.valkyrie_engine.rb create mode 100644 db/migrate/20260507181547_create_updated_at_index.valkyrie_engine.rb create mode 100644 db/migrate/20260507181548_add_optimistic_locking_to_orm_resources.valkyrie_engine.rb diff --git a/db/migrate/20260507181541_enable_uuid_extension.valkyrie_engine.rb b/db/migrate/20260507181541_enable_uuid_extension.valkyrie_engine.rb new file mode 100644 index 000000000..47d6d68ab --- /dev/null +++ b/db/migrate/20260507181541_enable_uuid_extension.valkyrie_engine.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +# This migration comes from valkyrie_engine (originally 20160111215816) +class EnableUuidExtension < ActiveRecord::Migration[5.0] + def change + enable_extension 'uuid-ossp' + end +end diff --git a/db/migrate/20260507181542_create_orm_resources.valkyrie_engine.rb b/db/migrate/20260507181542_create_orm_resources.valkyrie_engine.rb new file mode 100644 index 000000000..fd0e047cb --- /dev/null +++ b/db/migrate/20260507181542_create_orm_resources.valkyrie_engine.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +# This migration comes from valkyrie_engine (originally 20161007101725) +class CreateOrmResources < ActiveRecord::Migration[5.0] + def options + if ENV["VALKYRIE_ID_TYPE"] == "string" + { id: :text, default: -> { '(uuid_generate_v4())::text' } } + else + { id: :uuid } + end + end + + def change + create_table :orm_resources, **options do |t| + t.jsonb :metadata, null: false, default: {} + t.timestamps + end + add_index :orm_resources, :metadata, using: :gin + end +end diff --git a/db/migrate/20260507181543_add_model_type_to_orm_resources.valkyrie_engine.rb b/db/migrate/20260507181543_add_model_type_to_orm_resources.valkyrie_engine.rb new file mode 100644 index 000000000..1a5b40831 --- /dev/null +++ b/db/migrate/20260507181543_add_model_type_to_orm_resources.valkyrie_engine.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +# This migration comes from valkyrie_engine (originally 20170124135846) +class AddModelTypeToOrmResources < ActiveRecord::Migration[5.0] + def change + add_column :orm_resources, :resource_type, :string + end +end diff --git a/db/migrate/20260507181544_change_model_type_to_internal_model.valkyrie_engine.rb b/db/migrate/20260507181544_change_model_type_to_internal_model.valkyrie_engine.rb new file mode 100644 index 000000000..4084db54d --- /dev/null +++ b/db/migrate/20260507181544_change_model_type_to_internal_model.valkyrie_engine.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +# This migration comes from valkyrie_engine (originally 20170531004548) +class ChangeModelTypeToInternalModel < ActiveRecord::Migration[5.1] + def change + rename_column :orm_resources, :resource_type, :internal_resource + end +end diff --git a/db/migrate/20260507181545_create_path_gin_index.valkyrie_engine.rb b/db/migrate/20260507181545_create_path_gin_index.valkyrie_engine.rb new file mode 100644 index 000000000..59236db9c --- /dev/null +++ b/db/migrate/20260507181545_create_path_gin_index.valkyrie_engine.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +# This migration comes from valkyrie_engine (originally 20171011224121) +class CreatePathGinIndex < ActiveRecord::Migration[5.1] + def change + add_index :orm_resources, 'metadata jsonb_path_ops', using: :gin + end +end diff --git a/db/migrate/20260507181546_create_internal_resource_index.valkyrie_engine.rb b/db/migrate/20260507181546_create_internal_resource_index.valkyrie_engine.rb new file mode 100644 index 000000000..7e6898f3a --- /dev/null +++ b/db/migrate/20260507181546_create_internal_resource_index.valkyrie_engine.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +# This migration comes from valkyrie_engine (originally 20171204224121) +class CreateInternalResourceIndex < ActiveRecord::Migration[5.1] + def change + add_index :orm_resources, :internal_resource + end +end diff --git a/db/migrate/20260507181547_create_updated_at_index.valkyrie_engine.rb b/db/migrate/20260507181547_create_updated_at_index.valkyrie_engine.rb new file mode 100644 index 000000000..13998522f --- /dev/null +++ b/db/migrate/20260507181547_create_updated_at_index.valkyrie_engine.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +# This migration comes from valkyrie_engine (originally 20180212092225) +class CreateUpdatedAtIndex < ActiveRecord::Migration[5.1] + def change + add_index :orm_resources, :updated_at + end +end diff --git a/db/migrate/20260507181548_add_optimistic_locking_to_orm_resources.valkyrie_engine.rb b/db/migrate/20260507181548_add_optimistic_locking_to_orm_resources.valkyrie_engine.rb new file mode 100644 index 000000000..c91f96c0c --- /dev/null +++ b/db/migrate/20260507181548_add_optimistic_locking_to_orm_resources.valkyrie_engine.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +# This migration comes from valkyrie_engine (originally 20180802220739) +class AddOptimisticLockingToOrmResources < ActiveRecord::Migration[5.1] + def change + add_column :orm_resources, :lock_version, :integer + end +end diff --git a/db/structure.sql b/db/structure.sql index 84033ac74..225c82fbf 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1,7 +1,7 @@ -\restrict zGnP9xNrj5vXJP6eZcBchqhC4KoF4Tbn1lTESweMCy6CGbvkCQR9hwyeVf05jRP +\restrict 3tq1NbQsJWPHVS4qUFpTdLUpqfZeRSkN6zTmZeAcPmrO9aozielEKOgJcAmVCRD --- Dumped from database version 15.15 --- Dumped by pg_dump version 15.14 (Debian 15.14-0+deb12u1) +-- Dumped from database version 15.17 +-- Dumped by pg_dump version 15.16 (Debian 15.16-0+deb12u1) SET statement_timeout = 0; SET lock_timeout = 0; @@ -14,6 +14,20 @@ SET xmloption = content; SET client_min_messages = warning; SET row_security = off; +-- +-- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public; + + +-- +-- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)'; + + SET default_tablespace = ''; SET default_table_access_method = heap; @@ -996,6 +1010,20 @@ CREATE SEQUENCE public.minter_states_id_seq ALTER SEQUENCE public.minter_states_id_seq OWNED BY public.minter_states.id; +-- +-- Name: orm_resources; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.orm_resources ( + id uuid DEFAULT public.uuid_generate_v4() NOT NULL, + metadata jsonb DEFAULT '{}'::jsonb NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + internal_resource character varying, + lock_version integer +); + + -- -- Name: permission_template_accesses; Type: TABLE; Schema: public; Owner: - -- @@ -2801,6 +2829,14 @@ ALTER TABLE ONLY public.minter_states ADD CONSTRAINT minter_states_pkey PRIMARY KEY (id); +-- +-- Name: orm_resources orm_resources_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.orm_resources + ADD CONSTRAINT orm_resources_pkey PRIMARY KEY (id); + + -- -- Name: permission_template_accesses permission_template_accesses_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -3382,6 +3418,34 @@ CREATE INDEX index_mailboxer_receipts_on_receiver_id_and_receiver_type ON public CREATE UNIQUE INDEX index_minter_states_on_namespace ON public.minter_states USING btree (namespace); +-- +-- Name: index_orm_resources_on_internal_resource; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_orm_resources_on_internal_resource ON public.orm_resources USING btree (internal_resource); + + +-- +-- Name: index_orm_resources_on_metadata; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_orm_resources_on_metadata ON public.orm_resources USING gin (metadata); + + +-- +-- Name: index_orm_resources_on_metadata_jsonb_path_ops; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_orm_resources_on_metadata_jsonb_path_ops ON public.orm_resources USING gin (metadata jsonb_path_ops); + + +-- +-- Name: index_orm_resources_on_updated_at; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_orm_resources_on_updated_at ON public.orm_resources USING btree (updated_at); + + -- -- Name: index_permission_template_accesses_on_permission_template_id; Type: INDEX; Schema: public; Owner: - -- @@ -3873,7 +3937,7 @@ ALTER TABLE ONLY public.mailboxer_receipts -- PostgreSQL database dump complete -- -\unrestrict zGnP9xNrj5vXJP6eZcBchqhC4KoF4Tbn1lTESweMCy6CGbvkCQR9hwyeVf05jRP +\unrestrict 3tq1NbQsJWPHVS4qUFpTdLUpqfZeRSkN6zTmZeAcPmrO9aozielEKOgJcAmVCRD SET search_path TO "$user", public; @@ -3997,6 +4061,14 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240916182737'), ('20240916182823'), ('20241203010707'), -('20241205212513'); +('20241205212513'), +('20260507181541'), +('20260507181542'), +('20260507181543'), +('20260507181544'), +('20260507181545'), +('20260507181546'), +('20260507181547'), +('20260507181548'); From bc2ce0e314e80c999af7d76abea6cf3666cd22a7 Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Thu, 7 May 2026 15:08:19 -0400 Subject: [PATCH 03/38] initial model pass (needs controlled vocab/identifier support) --- config/initializers/wings.rb | 93 ++++++++- config/metadata/audio_visual_metadata.yaml | 16 +- spec/models/audio_visual_resource_spec.rb | 59 ++++++ spec/models/image_resource_spec.rb | 72 +++++++ spec/models/publication_resource_spec.rb | 43 +++++ spec/models/student_work_resource_spec.rb | 41 ++++ .../models/common_metadata_fields.rb | 180 ++++++++++++++++++ 7 files changed, 490 insertions(+), 14 deletions(-) create mode 100644 spec/models/audio_visual_resource_spec.rb create mode 100644 spec/models/image_resource_spec.rb create mode 100644 spec/models/publication_resource_spec.rb create mode 100644 spec/models/student_work_resource_spec.rb create mode 100644 spec/support/shared_examples/models/common_metadata_fields.rb diff --git a/config/initializers/wings.rb b/config/initializers/wings.rb index b47bea4bc..b64d6a2e4 100644 --- a/config/initializers/wings.rb +++ b/config/initializers/wings.rb @@ -1,10 +1,19 @@ # frozen_string_literal: true # -# Copied from Dassie example in Hyrax—register our models with Wings so they convert correctly +# Set up cribbed from the Dassie example app within Hyrax, which itself is adapted from Hyku. # -# @todo Revisit this when Valkyrizing. This might need to be moved to config/initializers/valkyrie.rb +# @note I have VALKYRIE_TRANSITION=true defined in .env.local but I'm unsure if it's neccessary? # @see https://github.com/samvera/hyrax/blob/hyrax-v5.2.0/.dassie/config/initializers/wings.rb Rails.application.config.after_initialize do + # active_fedora models we're migrating + [Publication, Image, StudentWork, AudioVisual].each do |work_type| + Wings::ModelRegistry.register("#{work_type}Resource".constantize, work_type) + + # from dassie: + # "we register itself so we can pre-translate the class in Freyja instead of having to translate in each query_service" + Wings::ModelRegistry.register(work_type, work_type) + end + Wings::ModelRegistry.register(Collection, Collection) Wings::ModelRegistry.register(AdminSet, AdminSet) Wings::ModelRegistry.register(FileSet, FileSet) @@ -12,8 +21,80 @@ Wings::ModelRegistry.register(Hydra::PCDM::File, Hydra::PCDM::File) Wings::ModelRegistry.register(Hyrax::FileMetadata, Hydra::PCDM::File) - Wings::ModelRegistry.register(Publication, PublicationResource) - Wings::ModelRegistry.register(Image, ImageResource) - Wings::ModelRegistry.register(StudentWork, StudentWorkResource) - Wings::ModelRegistry.register(AudioVisual, AudioVisualResource) + Valkyrie::MetadataAdapter.register(Freyja::MetadataAdapter.new, :freyja) + + Valkyrie::StorageAdapter.register( + Valkyrie::Storage::VersionedDisk.new( + base_path: Rails.root.join('storage', 'files'), + file_mover: FileUtils.method(:cp) + ), + :disk + ) + + Hyrax.config.query_index_from_valkyrie = true + Hyrax.config.index_adapter = :solr_index + + Valkyrie.config.metadata_adapter = :freyja + Valkyrie.config.storage_adapter = :disk + Valkyrie.config.indexing_adapter = :solr_index + + # load all the sql based custom queries + [ + Hyrax::CustomQueries::Navigators::CollectionMembers, + Hyrax::CustomQueries::Navigators::ChildCollectionsNavigator, + Hyrax::CustomQueries::Navigators::ParentCollectionsNavigator, + Hyrax::CustomQueries::Navigators::ChildFileSetsNavigator, + Hyrax::CustomQueries::Navigators::ChildWorksNavigator, + Hyrax::CustomQueries::Navigators::FindFiles, + Hyrax::CustomQueries::FindAccessControl, + Hyrax::CustomQueries::FindCollectionsByType, + Hyrax::CustomQueries::FindFileMetadata, + Hyrax::CustomQueries::FindIdsByModel, + Hyrax::CustomQueries::FindManyByAlternateIds, + Hyrax::CustomQueries::FindModelsByAccess, + Hyrax::CustomQueries::FindCountBy, + Hyrax::CustomQueries::FindByDateRange, + # Hyrax::CustomQueries::FindBySourceIdentifier # from bulkrax + ].each do |handler| + Hyrax.query_service.services[0].custom_queries.register_query_handler(handler) + end +end + +Rails.application.config.to_prepare do + # AdminSetResource.class_eval do + # attribute :internal_resource, Valkyrie::Types::Any.default("AdminSet"), internal: true + # end + + # CollectionResource.class_eval do + # attribute :internal_resource, Valkyrie::Types::Any.default("Collection"), internal: true + # end + + Valkyrie.config.resource_class_resolver = lambda do |resource_klass_name| + # TODO: Can we use some kind of lookup. + klass_name = resource_klass_name.gsub(/^Wings\((.+)\)$/, '\1') + klass_name = klass_name.gsub(/Resource$/, '') + if %w[ + GenericWork + ].include?(klass_name) + "#{klass_name}Resource".constantize + elsif 'Collection' == klass_name + CollectionResource + elsif 'AdminSet' == klass_name + AdminSetResource + # Without this mapping, we'll see cases of Postgres Valkyrie adapter attempting to write to + # Fedora. Yeah! + elsif 'Hydra::AccessControl' == klass_name + Hyrax::AccessControl + elsif 'FileSet' == klass_name + Hyrax::FileSet + elsif 'Hydra::AccessControls::Embargo' == klass_name + Hyrax::Embargo + elsif 'Hydra::AccessControls::Lease' == klass_name + Hyrax::Lease + elsif 'Hydra::PCDM::File' == klass_name + Hyrax::FileMetadata + else + klass_name.constantize + end + end end diff --git a/config/metadata/audio_visual_metadata.yaml b/config/metadata/audio_visual_metadata.yaml index b7690b6f5..0e2535856 100644 --- a/config/metadata/audio_visual_metadata.yaml +++ b/config/metadata/audio_visual_metadata.yaml @@ -1,4 +1,12 @@ attributes: + barcode: + predicate: https://schema.org/Barcode + type: string + multiple: true + form: + multiple: true + index_keys: + - barcode_ssim date: predicate: http://purl.org/dc/terms/date type: string @@ -57,11 +65,3 @@ attributes: multiple: true index_keys: - provenance_tesim - barcode: - predicate: https://schema.org/Barcode - type: string - multiple: true - form: - multiple: true - index_keys: - - barcode_ssim diff --git a/spec/models/audio_visual_resource_spec.rb b/spec/models/audio_visual_resource_spec.rb new file mode 100644 index 000000000..1844bc1ae --- /dev/null +++ b/spec/models/audio_visual_resource_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true +RSpec.describe AudioVisualResource, valkyrization: true do + subject(:resource) { described_class.new } + + describe 'metadata properties' do + # @see spec/support/shared_examples/models/common_metadata_fields.rb + it_behaves_like 'it has base metadata fields' + it_behaves_like 'it has core metadata fields' + + # audio visual metadata + it 'has barcodes' do + expect { resource.barcode = ['00000000'] } + .to change { resource.barcode } + .to contain_exactly('00000000') + end + + it 'has dates' do + expect { resource.date = ['2026-05-07'] } + .to change { resource.date } + .to contain_exactly('2026-05-07') + end + + it 'has date_associateds' do + expect { resource.date_associated = ['2026-05-07'] } + .to change { resource.date_associated } + .to contain_exactly('2026-05-07') + end + + it 'has inscriptions' do + expect { resource.inscription = ['inscribed text', 'another note'] } + .to change { resource.inscription } + .to contain_exactly('inscribed text', 'another note') + end + + it 'has original_item_extents' do + expect { resource.original_item_extent = ['9cm', '6oz'] } + .to change { resource.original_item_extent } + .to contain_exactly('9cm', '6oz') + end + + it 'has repository_locations' do + expect { resource.repository_location = ['in the back room'] } + .to change { resource.repository_location } + .to contain_exactly('in the back room') + end + + it 'has research_assistance' do + expect { resource.research_assistance = ['Student A.', 'Student B.'] } + .to change { resource.research_assistance } + .to contain_exactly('Student A.', 'Student B.') + end + + it 'has provenances' do + expect { resource.provenance = ['found it at a yard sale'] } + .to change { resource.provenance } + .to contain_exactly('found it at a yard sale') + end + end +end diff --git a/spec/models/image_resource_spec.rb b/spec/models/image_resource_spec.rb new file mode 100644 index 000000000..00977dab2 --- /dev/null +++ b/spec/models/image_resource_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true +RSpec.describe ImageResource, valkyrization: true do + subject(:resource) { described_class.new } + + describe 'metadata properties' do + # @see spec/support/shared_examples/models/common_metadata_fields.rb + it_behaves_like 'it has base metadata fields' + it_behaves_like 'it has core metadata fields' + + # image_metadata + it 'has dates' do + expect { resource.date = ['2026-05', '2026-05-07'] } + .to change { resource.date } + .to contain_exactly('2026-05', '2026-05-07') + end + + it 'has date_associateds' do + expect { resource.date_associated = ['2026-05-07', '2026-05-08'] } + .to change { resource.date_associated } + .to contain_exactly('2026-05-07', '2026-05-08') + end + + it 'has date_scope_notes' do + expect { resource.date_scope_note = ['a big day'] } + .to change { resource.date_scope_note } + .to contain_exactly('a big day') + end + + it 'has donors' do + expect { resource.donor = ['alumnus'] } + .to change { resource.donor } + .to contain_exactly('alumnus') + end + + it 'has inscriptions' do + expect { resource.inscription = ['a small note'] } + .to change { resource.inscription } + .to contain_exactly('a small note') + end + + it 'has original_item_extents' do + expect { resource.original_item_extent = ['9cm', '6oz'] } + .to change { resource.original_item_extent } + .to contain_exactly('9cm', '6oz') + end + + it 'has repository_locations' do + expect { resource.repository_location = ['in the back room'] } + .to change { resource.repository_location } + .to contain_exactly('in the back room') + end + + it 'has requested_bys' do + expect { resource.requested_by = ['student a', 'student b'] } + .to change { resource.requested_by } + .to contain_exactly('student a', 'student b') + end + + it 'has research_assistance' do + expect { resource.research_assistance = ['student c', 'student d'] } + .to change { resource.research_assistance } + .to contain_exactly('student c', 'student d') + end + + it 'has subject_ocms' do + expect { resource.subject_ocm = ['000 VALUE'] } + .to change { resource.subject_ocm } + .to contain_exactly('000 VALUE') + end + + end +end \ No newline at end of file diff --git a/spec/models/publication_resource_spec.rb b/spec/models/publication_resource_spec.rb new file mode 100644 index 000000000..d068bb258 --- /dev/null +++ b/spec/models/publication_resource_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +RSpec.describe PublicationResource, valkyrization: true do + subject(:resource) { described_class.new } + + describe 'metadata properties' do + # @see spec/support/shared_examples/models/common_metadata_fields.rb + it_behaves_like 'it has base metadata fields' + it_behaves_like 'it has core metadata fields' + it_behaves_like 'it has institutional metadata fields' + + describe 'publication metadata' do + it 'has abstracts' do + expect { resource.abstract = ['Short description'] } + .to change { resource.abstract } + .to eq ['Short description'] + end + + it 'has dates available' do + expect { resource.date_available = ['2026-05-07'] } + .to change { resource.date_available } + .to eq ['2026-05-07'] + end + + it 'has dates issued' do + expect { resource.date_issued = ['2026-05-07'] } + .to change { resource.date_issued } + .to eq ['2026-05-07'] + end + + it 'has an editor' do + expect { resource.editor = ['Ed. 1', 'Ed.2 '] } + .to change { resource.editor } + .to eq ['Ed. 1', 'Ed.2 '] + end + + it 'has an license' do + expect { resource.abstract = ['Some licensing info'] } + .to change { resource.abstract } + .to eq ['Some licensing info'] + end + end + end +end diff --git a/spec/models/student_work_resource_spec.rb b/spec/models/student_work_resource_spec.rb new file mode 100644 index 000000000..64092d856 --- /dev/null +++ b/spec/models/student_work_resource_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true +RSpec.describe StudentWorkResource, valkyrization: true do + subject(:resource) { described_class.new } + + describe 'metadata properties' do + # @see spec/support/shared_examples/models/common_metadata_fields.rb + it_behaves_like 'it has base metadata fields' + it_behaves_like 'it has core metadata fields' + + # student work metadata + it 'has abstracts' do + expect { resource.abstract = ['a brief description'] } + .to change { resource.abstract } + .to contain_exactly('a brief description') + end + + it 'has access_notes' do + expect { resource.access_note = ['admin access only'] } + .to change { resource.access_note } + .to contain_exactly('admin access only') + end + + it 'has advisors' do + expect { resource.advisor = ['Professor A', 'Professor B.'] } + .to change { resource.advisor } + .to contain_exactly('Professor A', 'Professor B.') + end + + it 'has dates' do + expect { resource.date = ['2026-05'] } + .to change { resource.date } + .to contain_exactly('2026-05') + end + + it 'has date_availables' do + expect { resource.date_available = ['2026-06-01'] } + .to change { resource.date_available } + .to contain_exactly('2026-06-01') + end + end +end diff --git a/spec/support/shared_examples/models/common_metadata_fields.rb b/spec/support/shared_examples/models/common_metadata_fields.rb new file mode 100644 index 000000000..a46b47dea --- /dev/null +++ b/spec/support/shared_examples/models/common_metadata_fields.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true +# +# Common metadata fields to test +# +# it_behaves_like 'it has base metadata fields' +# it_behaves_like 'it has core metadata fields' +RSpec.shared_examples 'it has base metadata fields' do + subject(:resource) { described_class.new } + + it 'has bibliographic_citations' do + expect { resource.bibliographic_citation = ['Work of art. Author', 'Another citation'] } + .to change { resource.bibliographic_citation } + .to contain_exactly('Work of art. Author', 'Another citation') + end + + it 'has contributors' do + expect { resource.contributor = ['contributor'] } + .to change { resource.contributor } + .to contain_exactly('contributor') + end + + it 'has creators' do + expect { resource.creator = ['A. Creator', 'another'] } + .to change { resource.creator } + .to contain_exactly('A. Creator', 'another') + end + + it 'has descriptions' do + expect { resource.description = ['the work'] } + .to change { resource.description } + .to contain_exactly('the work') + end + + it 'has identifiers' do + expect { resource.identifier = ['local:abc123', 'ldr:000000'] } + .to change { resource.identifier } + .to contain_exactly('local:abc123', 'ldr:000000') + end + + it 'has keywords' do + expect { resource.keyword = ['one', 'two'] } + .to change { resource.keyword } + .to contain_exactly('one', 'two') + end + + it 'has languages' do + expect { resource.language = ['en', 'fr'] } + .to change { resource.language } + .to contain_exactly('en', 'fr') + end + + it 'has locations' do + expect { resource.location = ['http://sws.geonames.org/5188140/'] } + .to change { resource.location } + .to contain_exactly('http://sws.geonames.org/5188140/') + end + + it 'has notes' do + expect { resource.note = ['note 1', 'note 2'] } + .to change { resource.note } + .to contain_exactly('note 1', 'note 2') + end + + it 'has physical_mediums' do + expect { resource.physical_medium = ['cd'] } + .to change { resource.physical_medium } + .to contain_exactly('cd') + end + + it 'has publishers' do + expect { resource.publisher = ['McGuffin'] } + .to change { resource.publisher } + .to contain_exactly('McGuffin') + end + + it 'has related_resources' do + expect { resource.related_resource = ['https://ldr.lafayette.edu'] } + .to change { resource.related_resource } + .to contain_exactly('https://ldr.lafayette.edu') + end + + it 'has resource_types' do + expect { resource.resource_type = ['Article', 'Other'] } + .to change { resource.resource_type } + .to contain_exactly('Article', 'Other') + end + + it 'has rights_holders' do + expect { resource.rights_holder = ['T. Owner'] } + .to change { resource.rights_holder } + .to contain_exactly('T. Owner') + end + + it 'has rights_statements' do + expect { resource.rights_statement = ['http://creativecommons.org/publicdomain/mark/1.0/'] } + .to change { resource.rights_statement } + .to contain_exactly('http://creativecommons.org/publicdomain/mark/1.0/') + end + + it 'has sources' do + expect { resource.source = ['Lafayette College'] } + .to change { resource.source } + .to contain_exactly('Lafayette College') + end + + it 'has source_identifiers' do + expect { resource.source_identifier = ['ldr:import:1'] } + .to change { resource.source_identifier } + .to contain_exactly('ldr:import:1') + end + + it 'has subjects' do + expect { resource.subject = ['http://id.worldcat.org/fast/1061714'] } + .to change { resource.subject } + .to contain_exactly('http://id.worldcat.org/fast/1061714') + end + + it 'has subtitles' do + expect { resource.subtitle = ['A great work'] } + .to change { resource.subtitle } + .to contain_exactly('A great work') + end + + it 'has title_alternatives' do + expect { resource.title_alternative = ['Aka One', 'Aka Two'] } + .to change { resource.title_alternative } + .to contain_exactly('Aka One', 'Aka Two') + end +end + +RSpec.shared_examples 'it has core metadata fields' do + subject(:resource) { described_class.new } + let(:date) { Time.zone.today } + + it 'has titles' do + expect { resource.title = ['Work title'] } + .to change { resource.title } + .to eq ['Work title'] + end + + it 'has a date_modified' do + expect { resource.date_modified = date } + .to change { resource.date_modified } + .to eq date + end + + it 'has a date_uploaded' do + expect { resource.date_uploaded = date } + .to change { resource.date_uploaded } + .to eq date + end + + it 'has a depositor' do + expect { resource.depositor = 'repository@lafayette.edu' } + .to change { resource.depositor } + .to eq 'repository@lafayette.edu' + end +end + +RSpec.shared_examples 'it has institutional metadata fields' do + subject(:resource) { described_class.new } + + it 'has academic_departments' do + expect { resource.academic_department = ['English', 'Library'] } + .to change { resource.academic_department } + .to contain_exactly 'English', 'Library' + end + + it 'has divisions' do + expect { resource.division = ['Sciences'] } + .to change { resource.division } + .to contain_exactly 'Sciences' + end + + it 'has a organization' do + expect { resource.organization = ['Lafayette College'] } + .to change { resource.organization } + .to contain_exactly 'Lafayette College' + end +end From 7b8053b018ab007d90c9f54fc2680b98cb4460bb Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Thu, 7 May 2026 16:23:30 -0400 Subject: [PATCH 04/38] first pass @ resource indexers --- app/indexers/audio_visual_resource_indexer.rb | 7 + app/indexers/base_resource_indexer.rb | 112 ++++++++++++++ app/indexers/image_resource_indexer.rb | 6 + app/indexers/publication_resource_indexer.rb | 68 +++++++++ app/indexers/student_work_resource_indexer.rb | 23 +++ config/metadata/base_metadata.yaml | 1 - .../publication_resource_indexer_spec.rb | 140 ++++++++++++++++++ 7 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 app/indexers/audio_visual_resource_indexer.rb create mode 100644 app/indexers/base_resource_indexer.rb create mode 100644 app/indexers/image_resource_indexer.rb create mode 100644 app/indexers/publication_resource_indexer.rb create mode 100644 app/indexers/student_work_resource_indexer.rb create mode 100644 spec/indexers/publication_resource_indexer_spec.rb diff --git a/app/indexers/audio_visual_resource_indexer.rb b/app/indexers/audio_visual_resource_indexer.rb new file mode 100644 index 000000000..ac407f39d --- /dev/null +++ b/app/indexers/audio_visual_resource_indexer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +class AudioVisualResourceIndexer < BaseResourceIndexer + include Hyrax::Indexer(:audio_visual_metadata) + + self.sortable_date_field = :date + self.years_encompassed_fields = [:date, :date_associated] +end diff --git a/app/indexers/base_resource_indexer.rb b/app/indexers/base_resource_indexer.rb new file mode 100644 index 000000000..c54d8feba --- /dev/null +++ b/app/indexers/base_resource_indexer.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true +# +# Indexer for properties common among our work types. +# +# @example +# class NewResourceIndexer < BaseResourceIndexer +# #... +# end +# +class BaseResourceIndexer < Hyrax::Indexers::PcdmObjectIndexer + include Hyrax::Indexer(:core_metadata) + include Hyrax::Indexer(:base_metadata) + + class_attribute :sortable_date_field, default: :date + class_attribute :years_encompassed_fields, default: [:date] + + # @todo update the HandleService to generate this and then call from here + # include Spot::IndexesPermalink + + def to_solr + super.tap do |doc| + doc.merge!( + citation_metadata, + language_and_label, + rights_statement_and_labels, + { + 'date_sort_dtsi' => parse_sortable_date, + 'years_encompassed_iim' => parse_years_encompassed + } + ) + end + end + + private + + def citation_metadata + return {} unless resource.respond_to?(:bibliographic_citation) + + raw = Array.wrap(resource.bibliographic_citation).first + parsed = ::AnyStyle.parse(raw)&.first + return {} if parsed.blank? || parsed[:type].nil? + + first_page, last_page = parsed[:pages]&.first&.split(/[-–—]/, 2) + + { + 'citation_journal_title_ss' => parsed[:'container-title']&.first, + 'citation_volume_ss' => parsed[:volume]&.first, + 'citation_issue_ss' => parsed[:issue]&.first, + 'citation_firstpage_ss' => first_page, + 'citation_lastpage_ss' => last_page + + } + end + + def language_and_label + return {} if resource.language.empty? + + { + 'language_ssim' => resource.language.map(&:to_s), + 'language_label_ssim' => resource.language.map { |lang| Spot::ISO6391.label_for(lang) } + } + end + + # Uses either the earliest date in metadata (using .sortable_date_field attribute) + # or falls back to the create_date of the resource to determine a date to use for sorting. + def parse_sortable_date + value = (resource.try(sortable_date_field) || []).sort.first + return if value.blank? + + parsed = Date.edtf(value) + parsed = parsed.first if parsed.class < ::Enumerable # guard for EDTF sets/intervals/etc + parsed ||= Date.parse(resource.create_date.to_s) + + parsed.strftime('%FT%TZ') + end + + # "Years Encompassed" meaning what years are covered by the metadata dates for a resource. + # Handles individual dates and EDTF ranges (so "2001/2003" encompasses "2001", "2002", "2003"). + # Used for the blacklight_range_limit plugin. + def parse_years_encompassed + fields = Array.wrap(years_encompassed_fields) + return [] unless fields.any? { |field| resource.respond_to?(field) } + + fields.map { |f| resource.try(f).try(:to_a) || [] } + .flatten + .reduce([]) { |dates, date| + parsed = Date.edtf(date) + next (dates + [parsed.year]) if parsed.is_a? Date + next dates if parsed.nil? || !parsed.respond_to?(:map) + + dates + parsed.map(&:year) + } + .sort + .uniq + end + + def rights_statement_and_labels + { + 'rights_statement_ssim' => rights_statement_uris, + 'rights_statement_label_ssim' => rights_statement_uris.map { |uri| rights_statement_service.label(uri) { uri } }, + 'rights_statement_shortcode_ssim' => rights_statement_uris.map { |uri| rights_statement_service.shortcode(uri) { nil } } + } + end + + def rights_statement_service + @rights_statement_service ||= Hyrax.config.rights_statement_service_class.new + end + + def rights_statement_uris + @rights_statement_uris ||= resource.rights_statement.map(&:to_s) + end +end diff --git a/app/indexers/image_resource_indexer.rb b/app/indexers/image_resource_indexer.rb new file mode 100644 index 000000000..e7938ad1a --- /dev/null +++ b/app/indexers/image_resource_indexer.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +class ImageResourceIndexer < BaseResourceIndexer + include Hyrax::Indexer(:image_metadata) + + self.years_encompassed_fields = [:date, :date_associated] +end diff --git a/app/indexers/publication_resource_indexer.rb b/app/indexers/publication_resource_indexer.rb new file mode 100644 index 000000000..8e2e107b2 --- /dev/null +++ b/app/indexers/publication_resource_indexer.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true +class PublicationResourceIndexer < BaseResourceIndexer + include Hyrax::Indexer(:publication_metadata) + include Hyrax::Indexer(:institutional_metadata) + + self.sortable_date_field = :date_issued + self.years_encompassed_fields = [:date_issued] + + def to_solr + super.tap do |solr_doc| + solr_doc['english_language_date_teim'] = parsed_english_language_dates + + # @todo how are we handling full-text extraction/searching? + # solr_doc['extracted_text_tsimv'] = object.file_sets.map { |fs| fs.extracted_text.present? ? fs.extracted_text.content.strip : '' } + end + end + + private + + # Parses values in :date_issued and converts them to: + # - (Spring|Summer|Autumn/Fall|Winter) YYYY + # - Month YYYY + # - Mo YYYY + # + # @example for a resource with :date_issued February 11, 1986 + # #=> ['Winter 1986', 'February 1986', 'Feb 1986'] + # + # @example for a resource with :date_issued October 21, 2023 + # #=> ['Autumn 2023', 'Fall 2023', 'October 2023', 'Oct 2023'] + # + def parsed_english_language_dates + (resource.try(:date_issued) || []).map do |date| + begin + parsed = Date.parse(date) + rescue ArgumentError + next unless date.to_s.match?(/^\d{4}-\d{2}/) + parsed = Date.new(*date.to_s.split('-').map(&:to_i)) + end + + season_names_for_date(parsed) + full_and_abbreviated_months_for_date(parsed) + end.flatten.uniq + end + + # Determines the season based on the month: + # Spring => March, April, May + # Summer => June, July, August + # Autumn/Fall => September, October, November + # Winter => December, January, February + def season_names_for_date(date) + seasons = case date.strftime('%-m').to_i + when 3..5 then %w[Spring] + when 6..8 then %w[Summer] + when 9..11 then %w[Autumn Fall] + else %w[Winter] + end + year = date.year + seasons.map { |season| "#{season} #{year}" } + end + + # Transforms our date into English-language dates. + # + # @example + # full_and_abbreviated_months_for_date(Date.parse('2019-02-08')) + # #=> ['February 8 2019', 'Feb 8 2019'] + def full_and_abbreviated_months_for_date(date) + %w[%B %b].map { |month| date.strftime("#{month} %Y") } + end +end \ No newline at end of file diff --git a/app/indexers/student_work_resource_indexer.rb b/app/indexers/student_work_resource_indexer.rb new file mode 100644 index 000000000..b1968ff4f --- /dev/null +++ b/app/indexers/student_work_resource_indexer.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +class StudentWorkResourceIndexer < BaseResourceIndexer + include Hyrax::Indexer(:student_work_metadata) + include Hyrax::Indexer(:institutional_metadata) + + self.sortable_date_field = :date + self.years_encompassed_fields = [:date] + + def to_solr + super.tap do |solr_doc| + solr_doc['advisor_ssim'] = object.advisor.to_a + solr_doc['advisor_label_ssim'] = object.advisor.map { |email| advisor_label_from(email: email) } + end + end + + private + + def advisor_label_from(email:) + return email unless email.end_with?('@lafayette.edu') + + Spot::LafayetteInstructorsAuthorityService.label_for(email: email) + end +end diff --git a/config/metadata/base_metadata.yaml b/config/metadata/base_metadata.yaml index 7e86872ab..4283f6dae 100644 --- a/config/metadata/base_metadata.yaml +++ b/config/metadata/base_metadata.yaml @@ -51,7 +51,6 @@ attributes: index_keys: - keyword_tesim - keyword_sim - # @see IndexesLanguageAndLabel mixin for indexing info, uses language: predicate: http://purl.org/dc/elements/1.1/language type: string diff --git a/spec/indexers/publication_resource_indexer_spec.rb b/spec/indexers/publication_resource_indexer_spec.rb new file mode 100644 index 000000000..243d8d4f9 --- /dev/null +++ b/spec/indexers/publication_resource_indexer_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true +RSpec.describe PublicationResourceIndexer, valkyrization: true do + subject(:solr_document) { described_class.for(resource: resource).to_solr } + + let(:default_thumbnail_path) { ActionController::Base.helpers.image_path('default.png').to_s } + let(:resource) { PublicationResource.new(**metadata) } + let(:date) { Time.zone.today } + let(:metadata) do + { + # core metadata + title: ['Title of Work'], + date_modified: date, + date_uploaded: date, + depositor: 'repository@lafayette.edu', + + # base metadata + bibliographic_citation: ['Last, First. "Title." Journal 1.2 (2000): 1-2.'], + contributor: ['Contributor A', 'Contributor B'], + creator: ['Malantonio, Anna'], + description: ['Description of work'], + identifier: ['local:abc123'], + keyword: ['libraries', 'test'], + language: ['en'], + location: ['http://sws.geonames.org/5188140/'], + note: ['A note about the thing'], + physical_medium: ['none'], + publisher: ['Great Thoughts Pub'], + related_resource: ['https://ldr.lafayette.edux'], + resource_type: ['Article', 'Other'], + rights_holder: ['Malantonio, Anna'], + rights_statement: ['http://creativecommons.org/publicdomain/mark/1.0/'], + source: ['Lafayette College'], + source_identifier: ['test.1.1'], + subtitle: ['a curious work'], + title_alternative: ['another name'], + + # institutional metadata + academic_department: ['Libraries'], + division: ['Humanities'], + organization: ['Lafayette College'], + + # publication metadata + abstract: ['A short description'], + date_issued: ['2026-05-07'], + date_available: ['2026-05-07'], + editor: ['Editor, Anne'], + license: ['Some licensing info'] + } + end + + # @todo add location + subject URI handling + # rubocop:disable Layout/FirstHashElementIndentation + it 'generates a solr document' do + expect(solr_document).to eq({ + abstract_tesim: ['A short description'], + academic_department_sim: ['Libraries'], + academic_department_tesim: ['Libraries'], + admin_set_id_ssim: [''], # hyrax-managed field + admin_set_sim: nil, # hyrax-managed field + admin_set_tesim: nil, # hyrax-managed field + alternate_ids_sim: [], # hyrax-managed field + bibliographic_citation_tesim: ['Last, First. "Title." Journal 1.2 (2000): 1-2.'], + citation_firstpage_ss: '1', + citation_issue_ss: '2', + citation_journal_title_ss: 'Journal', + citation_lastpage_ss: '2', + citation_volume_ss: '1', + contributor_tesim: ['Contributor A', 'Contributor B'], + contributor_sim: ['Contributor A', 'Contributor B'], + creator_tesim: ['Malantonio, Anna'], + creator_sim: ['Malantonio, Anna'], + date_issued_ssim: ['2026-05-07'], + date_available_ssim: ['2026-05-07'], + date_modified_dtsi: nil, # Hyrax-managed field + date_sort_dtsi: '2026-05-07T00:00:00Z', + date_uploaded_dtsi: nil, # not applied before save + depositor_ssim: ['repository@lafayette.edu'], # Hyrax-managed field + depositor_tesim: ['repository@lafayette.edu'], # Hyrax-managed field + description_tesim: ['Description of work'], + division_sim: ['Humanities'], + division_tesim: ['Humanities'], + edit_access_group_ssim: [], # Hyrax-managed field + edit_access_person_ssim: [], # Hyrax-managed field + editor_sim: ['Editor, Anne'], + editor_tesim: ['Editor, Anne'], + embargo_history_ssim: nil, # Hyrax-managed field + english_language_date_teim: ['Spring 2026', 'May 2026'], + generic_type_si: 'Work', # Hyrax-managed field + hasRelatedImage_ssim: [''], # Hyrax-managed field + hasRelatedMediaFragment_ssim: [''], # Hyrax-managed field + has_model_ssim: 'PublicationResource', # Hyrax-managed field + human_readable_type_sim: 'Publication Resource', # Hyrax-managed field + human_readable_type_tesim: 'Publication Resource', # Hyrax-managed field + id: '', # Hyrax-managed field + identifier_ssim: ['local:abc123'], + isPartOf_ssim: [''], # Hyrax-managed field + keyword_tesim: ['libraries', 'test'], + keyword_sim: ['libraries', 'test'], + language_ssim: ['en'], + language_label_ssim: ['English'], + lease_history_ssim: nil, # Hyrax-managed field + license_tsm: ['Some licensing info'], + member_ids_ssim: [], # Hyrax-managed field + member_of_collection_ids_ssim: [], # Hyrax-managed field + note_tesim: ['A note about the thing'], + organization_sim: ['Lafayette College'], + organization_tesim: ['Lafayette College'], + physical_medium_sim: ['none'], + physical_medium_tesim: ['none'], + publisher_sim: ['Great Thoughts Pub'], + publisher_tesim: ['Great Thoughts Pub'], + read_access_group_ssim: [], # Hyrax-managed field + read_access_person_ssim: [], # Hyrax-managed field + related_resource_sim: ['https://ldr.lafayette.edux'], + related_resource_tesim: ['https://ldr.lafayette.edux'], + resource_type_ssim: ['Article', 'Other'], + rights_holder_tesim: ['Malantonio, Anna'], + rights_holder_sim: ['Malantonio, Anna'], + rights_statement_ssim: ['http://creativecommons.org/publicdomain/mark/1.0/'], + rights_statement_label_ssim: ['Public Domain Mark'], + rights_statement_shortcode_ssim: ['PDM'], + source_tesim: ['Lafayette College'], + source_sim: ['Lafayette College'], + source_identifier_ssim: ['test.1.1'], + subtitle_tesim: ['a curious work'], + subtitle_sim: ['a curious work'], + suppressed_bsi: false, # Hyrax-managed field + system_create_dtsi: nil, # Hyrax-managed field + system_modified_dtsi: nil, # Hyrax-managed field + thumbnail_path_ss: default_thumbnail_path, # Hyrax-managed field + title_sim: ['Title of Work'], + title_tesim: ['Title of Work'], + title_alternative_tesim: ['another name'], + title_alternative_sim: ['another name'], + visibility_ssi: 'restricted', # Hyrax-managed field + years_encompassed_iim: [2026] + }.with_indifferent_access) + end + # rubocop:enable Layout/FirstHashElementIndentation +end From 141dc130ea09f5dc0af1ce2c0bd6399fd2ab93af Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Fri, 8 May 2026 12:06:00 -0400 Subject: [PATCH 05/38] initial resource indexer specs --- app/indexers/student_work_resource_indexer.rb | 4 +- config/metadata/student_work_metadata.yaml | 1 + .../audio_visual_resource_indexer_spec.rb | 135 +++++++++++++++++ spec/indexers/image_resource_indexer_spec.rb | 140 +++++++++++++++++ .../publication_resource_indexer_spec.rb | 5 +- .../student_work_resource_indexer_spec.rb | 141 ++++++++++++++++++ 6 files changed, 422 insertions(+), 4 deletions(-) create mode 100644 spec/indexers/audio_visual_resource_indexer_spec.rb create mode 100644 spec/indexers/image_resource_indexer_spec.rb create mode 100644 spec/indexers/student_work_resource_indexer_spec.rb diff --git a/app/indexers/student_work_resource_indexer.rb b/app/indexers/student_work_resource_indexer.rb index b1968ff4f..fca90e818 100644 --- a/app/indexers/student_work_resource_indexer.rb +++ b/app/indexers/student_work_resource_indexer.rb @@ -8,8 +8,8 @@ class StudentWorkResourceIndexer < BaseResourceIndexer def to_solr super.tap do |solr_doc| - solr_doc['advisor_ssim'] = object.advisor.to_a - solr_doc['advisor_label_ssim'] = object.advisor.map { |email| advisor_label_from(email: email) } + solr_doc['advisor_ssim'] = (resource.try(:advisor) || []).to_a + solr_doc['advisor_label_ssim'] = (resource.try(:advisor) || []).map { |email| advisor_label_from(email: email) } end end diff --git a/config/metadata/student_work_metadata.yaml b/config/metadata/student_work_metadata.yaml index 39c697708..db9bcc5b6 100644 --- a/config/metadata/student_work_metadata.yaml +++ b/config/metadata/student_work_metadata.yaml @@ -23,6 +23,7 @@ attributes: multiple: true index_keys: - advisor_ssim + - advisor_tesim # dates are edtf values, is there a way we can make this a type? date: predicate: http://purl.org/dc/terms/date diff --git a/spec/indexers/audio_visual_resource_indexer_spec.rb b/spec/indexers/audio_visual_resource_indexer_spec.rb new file mode 100644 index 000000000..864360e1c --- /dev/null +++ b/spec/indexers/audio_visual_resource_indexer_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true +# +# @todo add location + subject URI handling +RSpec.describe AudioVisualResourceIndexer, valkyrization: true do + subject(:solr_document) { described_class.for(resource: resource).to_solr } + let(:resource) { AudioVisualResource.new(**metadata) } + + let(:default_thumbnail_path) { ActionController::Base.helpers.image_path('default.png').to_s } + let(:date) { Time.zone.today } + let(:metadata) do + { + # core metadata + title: ['Title of Work'], + date_modified: date, + date_uploaded: date, + depositor: 'repository@lafayette.edu', + + # base metadata + bibliographic_citation: ['Last, First. "Title." Journal 1.2 (2000): 1-2.'], + contributor: ['Contributor A', 'Contributor B'], + creator: ['Malantonio, Anna'], + description: ['Description of work'], + identifier: ['local:abc123'], + keyword: ['libraries', 'test'], + language: ['en'], + location: ['http://sws.geonames.org/5188140/'], + note: ['A note about the thing'], + physical_medium: ['none'], + publisher: ['Great Thoughts Pub'], + related_resource: ['https://ldr.lafayette.edux'], + resource_type: ['Video', 'Other'], + rights_holder: ['Malantonio, Anna'], + rights_statement: ['http://creativecommons.org/publicdomain/mark/1.0/'], + source: ['Lafayette College'], + source_identifier: ['test.1.1'], + subtitle: ['a curious work'], + title_alternative: ['another name'], + + # audio_visual metadata + barcode: ['00000000'], + date: ['2026-05-08'], + date_associated: ['2026-05-08'], + inscription: ['a note on the back'], + original_item_extent: ['9cm', '6oz'], + repository_location: ['in the back room'], + research_assistance: ['yes', 'we did'], + provenance: ['found it online'] + } + end + + # rubocop:disable Layout/FirstHashElementIndentation + it 'generates a solr document' do + expect(solr_document).to eq({ + admin_set_id_ssim: [''], # hyrax-managed field + admin_set_sim: nil, # hyrax-managed field + admin_set_tesim: nil, # hyrax-managed field + alternate_ids_sim: [], # hyrax-managed field + barcode_ssim: ['00000000'], + bibliographic_citation_tesim: ['Last, First. "Title." Journal 1.2 (2000): 1-2.'], + citation_firstpage_ss: '1', + citation_issue_ss: '2', + citation_journal_title_ss: 'Journal', + citation_lastpage_ss: '2', + citation_volume_ss: '1', + contributor_tesim: ['Contributor A', 'Contributor B'], + contributor_sim: ['Contributor A', 'Contributor B'], + creator_tesim: ['Malantonio, Anna'], + creator_sim: ['Malantonio, Anna'], + date_ssim: ['2026-05-08'], + date_associated_ssim: ['2026-05-08'], + date_associated_tesim: ['2026-05-08'], + date_modified_dtsi: nil, # Hyrax-managed field + date_sort_dtsi: '2026-05-08T00:00:00Z', + date_uploaded_dtsi: nil, # not applied before save + depositor_ssim: ['repository@lafayette.edu'], # Hyrax-managed field + depositor_tesim: ['repository@lafayette.edu'], # Hyrax-managed field + description_tesim: ['Description of work'], + edit_access_group_ssim: [], # Hyrax-managed field + edit_access_person_ssim: [], # Hyrax-managed field + embargo_history_ssim: nil, # Hyrax-managed field + generic_type_si: 'Work', # Hyrax-managed field + hasRelatedImage_ssim: [''], # Hyrax-managed field + hasRelatedMediaFragment_ssim: [''], # Hyrax-managed field + has_model_ssim: 'AudioVisualResource', # Hyrax-managed field + human_readable_type_sim: 'Audio Visual Resource', # Hyrax-managed field + human_readable_type_tesim: 'Audio Visual Resource', # Hyrax-managed field + id: '', # Hyrax-managed field + identifier_ssim: ['local:abc123'], + inscription_tesim: ['a note on the back'], + isPartOf_ssim: [''], # Hyrax-managed field + keyword_tesim: ['libraries', 'test'], + keyword_sim: ['libraries', 'test'], + language_ssim: ['en'], + language_label_ssim: ['English'], + lease_history_ssim: nil, # Hyrax-managed field + member_ids_ssim: [], # Hyrax-managed field + member_of_collection_ids_ssim: [], # Hyrax-managed field + note_tesim: ['A note about the thing'], + original_item_extent_tesim: ['9cm', '6oz'], + physical_medium_sim: ['none'], + physical_medium_tesim: ['none'], + provenance_tesim: ['found it online'], + publisher_sim: ['Great Thoughts Pub'], + publisher_tesim: ['Great Thoughts Pub'], + read_access_group_ssim: [], # Hyrax-managed field + read_access_person_ssim: [], # Hyrax-managed field + related_resource_sim: ['https://ldr.lafayette.edux'], + related_resource_tesim: ['https://ldr.lafayette.edux'], + repository_location_ssim: ['in the back room'], + research_assistance_ssim: ['yes', 'we did'], + resource_type_ssim: ['Video', 'Other'], + rights_holder_tesim: ['Malantonio, Anna'], + rights_holder_sim: ['Malantonio, Anna'], + rights_statement_ssim: ['http://creativecommons.org/publicdomain/mark/1.0/'], + rights_statement_label_ssim: ['Public Domain Mark'], + rights_statement_shortcode_ssim: ['PDM'], + source_tesim: ['Lafayette College'], + source_sim: ['Lafayette College'], + source_identifier_ssim: ['test.1.1'], + subtitle_tesim: ['a curious work'], + subtitle_sim: ['a curious work'], + suppressed_bsi: false, # Hyrax-managed field + system_create_dtsi: nil, # Hyrax-managed field + system_modified_dtsi: nil, # Hyrax-managed field + thumbnail_path_ss: default_thumbnail_path, # Hyrax-managed field + title_sim: ['Title of Work'], + title_tesim: ['Title of Work'], + title_alternative_tesim: ['another name'], + title_alternative_sim: ['another name'], + visibility_ssi: 'restricted', # Hyrax-managed field + years_encompassed_iim: [2026] + }.with_indifferent_access) + end + # rubocop:enable Layout/FirstHashElementIndentation +end diff --git a/spec/indexers/image_resource_indexer_spec.rb b/spec/indexers/image_resource_indexer_spec.rb new file mode 100644 index 000000000..e16ff998a --- /dev/null +++ b/spec/indexers/image_resource_indexer_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true +# +# @todo add location + subject URI handling +RSpec.describe ImageResourceIndexer, valkyrization: true do + subject(:solr_document) { described_class.for(resource: resource).to_solr } + let(:resource) { ImageResource.new(**metadata) } + + let(:default_thumbnail_path) { ActionController::Base.helpers.image_path('default.png').to_s } + let(:date) { Time.zone.today } + let(:metadata) do + { + # core metadata + title: ['Title of Image'], + date_modified: date, + date_uploaded: date, + depositor: 'repository@lafayette.edu', + + # base metadata + bibliographic_citation: ['Last, First. "Title." Journal 1.2 (2000): 1-2.'], + contributor: ['Contributor A', 'Contributor B'], + creator: ['Malantonio, Anna'], + description: ['Description of work'], + identifier: ['local:abc123'], + keyword: ['libraries', 'test'], + language: ['en'], + location: ['http://sws.geonames.org/5188140/'], + note: ['A note about the thing'], + physical_medium: ['none'], + publisher: ['Great Thoughts Pub'], + related_resource: ['https://ldr.lafayette.edux'], + resource_type: ['Article', 'Other'], + rights_holder: ['Malantonio, Anna'], + rights_statement: ['http://creativecommons.org/publicdomain/mark/1.0/'], + source: ['Lafayette College'], + source_identifier: ['test.1.1'], + subtitle: ['a curious work'], + title_alternative: ['another name'], + + # image_metadata + date: ['2026-05-08'], + date_associated: ['2026-05-08'], + date_scope_note: ['info about the date', 'more info'], + donor: ['A Generous Donor'], + inscription: ['a lil note'], + original_item_extent: ['9cm', '6oz'], + repository_location: ['in the back room'], + requested_by: ['A Curious Student'], + research_assistance: ['Student A', 'Student B'], + subject_ocm: ['000 VALUE'] + } + end + + # rubocop:disable Layout/FirstHashElementIndentation + it 'generates a solr document' do + expect(solr_document).to eq({ + admin_set_id_ssim: [''], # hyrax-managed field + admin_set_sim: nil, # hyrax-managed field + admin_set_tesim: nil, # hyrax-managed field + alternate_ids_sim: [], # hyrax-managed field + bibliographic_citation_tesim: ['Last, First. "Title." Journal 1.2 (2000): 1-2.'], + citation_firstpage_ss: '1', + citation_issue_ss: '2', + citation_journal_title_ss: 'Journal', + citation_lastpage_ss: '2', + citation_volume_ss: '1', + contributor_tesim: ['Contributor A', 'Contributor B'], + contributor_sim: ['Contributor A', 'Contributor B'], + creator_tesim: ['Malantonio, Anna'], + creator_sim: ['Malantonio, Anna'], + date_ssim: ['2026-05-08'], + date_associated_ssim: ['2026-05-08'], # @todo should this be _sim? + date_associated_tesim: ['2026-05-08'], + date_modified_dtsi: nil, # Hyrax-managed field + date_scope_note_tesim: ['info about the date', 'more info'], + date_sort_dtsi: '2026-05-08T00:00:00Z', + date_uploaded_dtsi: nil, # not applied before save + depositor_ssim: ['repository@lafayette.edu'], # Hyrax-managed field + depositor_tesim: ['repository@lafayette.edu'], # Hyrax-managed field + description_tesim: ['Description of work'], + donor_ssim: ['A Generous Donor'], + edit_access_group_ssim: [], # Hyrax-managed field + edit_access_person_ssim: [], # Hyrax-managed field + embargo_history_ssim: nil, # Hyrax-managed field + generic_type_si: 'Work', # Hyrax-managed field + hasRelatedImage_ssim: [''], # Hyrax-managed field + hasRelatedMediaFragment_ssim: [''], # Hyrax-managed field + has_model_ssim: 'ImageResource', # Hyrax-managed field + human_readable_type_sim: 'Image Resource', # Hyrax-managed field + human_readable_type_tesim: 'Image Resource', # Hyrax-managed field + id: '', # Hyrax-managed field + identifier_ssim: ['local:abc123'], + inscription_tesim: ['a lil note'], + isPartOf_ssim: [''], # Hyrax-managed field + keyword_tesim: ['libraries', 'test'], + keyword_sim: ['libraries', 'test'], + language_ssim: ['en'], + language_label_ssim: ['English'], + lease_history_ssim: nil, # Hyrax-managed field + member_ids_ssim: [], # Hyrax-managed field + member_of_collection_ids_ssim: [], # Hyrax-managed field + note_tesim: ['A note about the thing'], + original_item_extent_tesim: ['9cm', '6oz'], + physical_medium_sim: ['none'], + physical_medium_tesim: ['none'], + publisher_sim: ['Great Thoughts Pub'], + publisher_tesim: ['Great Thoughts Pub'], + read_access_group_ssim: [], # Hyrax-managed field + read_access_person_ssim: [], # Hyrax-managed field + related_resource_sim: ['https://ldr.lafayette.edux'], + related_resource_tesim: ['https://ldr.lafayette.edux'], + repository_location_ssim: ['in the back room'], + requested_by_ssim: ['A Curious Student'], + research_assistance_ssim: ['Student A', 'Student B'], + resource_type_ssim: ['Article', 'Other'], + rights_holder_tesim: ['Malantonio, Anna'], + rights_holder_sim: ['Malantonio, Anna'], + rights_statement_ssim: ['http://creativecommons.org/publicdomain/mark/1.0/'], + rights_statement_label_ssim: ['Public Domain Mark'], + rights_statement_shortcode_ssim: ['PDM'], + source_tesim: ['Lafayette College'], + source_sim: ['Lafayette College'], + source_identifier_ssim: ['test.1.1'], + subject_ocm_ssim: ['000 VALUE'], + subject_ocm_tesim: ['000 VALUE'], + subtitle_tesim: ['a curious work'], + subtitle_sim: ['a curious work'], + suppressed_bsi: false, # Hyrax-managed field + system_create_dtsi: nil, # Hyrax-managed field + system_modified_dtsi: nil, # Hyrax-managed field + thumbnail_path_ss: default_thumbnail_path, # Hyrax-managed field + title_sim: ['Title of Image'], + title_tesim: ['Title of Image'], + title_alternative_tesim: ['another name'], + title_alternative_sim: ['another name'], + visibility_ssi: 'restricted', # Hyrax-managed field + years_encompassed_iim: [2026] + }.with_indifferent_access) + end + # rubocop:enable Layout/FirstHashElementIndentation +end diff --git a/spec/indexers/publication_resource_indexer_spec.rb b/spec/indexers/publication_resource_indexer_spec.rb index 243d8d4f9..6159cca17 100644 --- a/spec/indexers/publication_resource_indexer_spec.rb +++ b/spec/indexers/publication_resource_indexer_spec.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true +# +# @todo add location + subject URI handling RSpec.describe PublicationResourceIndexer, valkyrization: true do subject(:solr_document) { described_class.for(resource: resource).to_solr } + let(:resource) { PublicationResource.new(**metadata) } let(:default_thumbnail_path) { ActionController::Base.helpers.image_path('default.png').to_s } - let(:resource) { PublicationResource.new(**metadata) } let(:date) { Time.zone.today } let(:metadata) do { @@ -48,7 +50,6 @@ } end - # @todo add location + subject URI handling # rubocop:disable Layout/FirstHashElementIndentation it 'generates a solr document' do expect(solr_document).to eq({ diff --git a/spec/indexers/student_work_resource_indexer_spec.rb b/spec/indexers/student_work_resource_indexer_spec.rb new file mode 100644 index 000000000..eb67448a6 --- /dev/null +++ b/spec/indexers/student_work_resource_indexer_spec.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true +# +# @todo add location + subject URI handling +RSpec.describe StudentWorkResourceIndexer, valkyrization: true do + subject(:solr_document) { described_class.for(resource: resource).to_solr } + let(:resource) { StudentWorkResource.new(**metadata) } + + let(:default_thumbnail_path) { ActionController::Base.helpers.image_path('default.png').to_s } + let(:date) { Time.zone.today } + let(:metadata) do + { + # core metadata + title: ['Title of Work'], + date_modified: date, + date_uploaded: date, + depositor: 'repository@lafayette.edu', + + # base metadata + bibliographic_citation: ['Last, First. "Title." Journal 1.2 (2000): 1-2.'], + contributor: ['Contributor A', 'Contributor B'], + creator: ['Malantonio, Anna'], + description: ['Description of work'], + identifier: ['local:abc123'], + keyword: ['libraries', 'test'], + language: ['en'], + location: ['http://sws.geonames.org/5188140/'], + note: ['A note about the thing'], + physical_medium: ['none'], + publisher: ['Great Thoughts Pub'], + related_resource: ['https://ldr.lafayette.edux'], + resource_type: ['Article', 'Other'], + rights_holder: ['Malantonio, Anna'], + rights_statement: ['http://creativecommons.org/publicdomain/mark/1.0/'], + source: ['Lafayette College'], + source_identifier: ['test.1.1'], + subtitle: ['a curious work'], + title_alternative: ['another name'], + + # institutional metadata + academic_department: ['Libraries'], + division: ['Humanities'], + organization: ['Lafayette College'], + + # student_work metadata + abstract: ['A short description'], + advisor: ['Professor, A'], + access_note: ['upon request only'], + date: ['2026-05-08'], + date_available: ['2026-05-08'], + } + end + + # rubocop:disable Layout/FirstHashElementIndentation + it 'generates a solr document' do + expect(solr_document).to eq({ + abstract_tesim: ['A short description'], + access_note_tesim: ['upon request only'], + academic_department_sim: ['Libraries'], + academic_department_tesim: ['Libraries'], + admin_set_id_ssim: [''], # hyrax-managed field + admin_set_sim: nil, # hyrax-managed field + admin_set_tesim: nil, # hyrax-managed field + advisor_ssim: ['Professor, A'], + advisor_tesim: ['Professor, A'], + advisor_label_ssim: ['Professor, A'], + alternate_ids_sim: [], # hyrax-managed field + bibliographic_citation_tesim: ['Last, First. "Title." Journal 1.2 (2000): 1-2.'], + citation_firstpage_ss: '1', + citation_issue_ss: '2', + citation_journal_title_ss: 'Journal', + citation_lastpage_ss: '2', + citation_volume_ss: '1', + contributor_tesim: ['Contributor A', 'Contributor B'], + contributor_sim: ['Contributor A', 'Contributor B'], + creator_tesim: ['Malantonio, Anna'], + creator_sim: ['Malantonio, Anna'], + date_ssim: ['2026-05-08'], + date_available_ssim: ['2026-05-08'], + date_modified_dtsi: nil, # Hyrax-managed field + date_sort_dtsi: '2026-05-08T00:00:00Z', + date_uploaded_dtsi: nil, # not applied before save + depositor_ssim: ['repository@lafayette.edu'], # Hyrax-managed field + depositor_tesim: ['repository@lafayette.edu'], # Hyrax-managed field + description_tesim: ['Description of work'], + division_sim: ['Humanities'], + division_tesim: ['Humanities'], + edit_access_group_ssim: [], # Hyrax-managed field + edit_access_person_ssim: [], # Hyrax-managed field + embargo_history_ssim: nil, # Hyrax-managed field + generic_type_si: 'Work', # Hyrax-managed field + hasRelatedImage_ssim: [''], # Hyrax-managed field + hasRelatedMediaFragment_ssim: [''], # Hyrax-managed field + has_model_ssim: 'StudentWorkResource', # Hyrax-managed field + human_readable_type_sim: 'Student Work Resource', # Hyrax-managed field + human_readable_type_tesim: 'Student Work Resource', # Hyrax-managed field + id: '', # Hyrax-managed field + identifier_ssim: ['local:abc123'], + isPartOf_ssim: [''], # Hyrax-managed field + keyword_tesim: ['libraries', 'test'], + keyword_sim: ['libraries', 'test'], + language_ssim: ['en'], + language_label_ssim: ['English'], + lease_history_ssim: nil, # Hyrax-managed field + member_ids_ssim: [], # Hyrax-managed field + member_of_collection_ids_ssim: [], # Hyrax-managed field + note_tesim: ['A note about the thing'], + organization_sim: ['Lafayette College'], + organization_tesim: ['Lafayette College'], + physical_medium_sim: ['none'], + physical_medium_tesim: ['none'], + publisher_sim: ['Great Thoughts Pub'], + publisher_tesim: ['Great Thoughts Pub'], + read_access_group_ssim: [], # Hyrax-managed field + read_access_person_ssim: [], # Hyrax-managed field + related_resource_sim: ['https://ldr.lafayette.edux'], + related_resource_tesim: ['https://ldr.lafayette.edux'], + resource_type_ssim: ['Article', 'Other'], + rights_holder_tesim: ['Malantonio, Anna'], + rights_holder_sim: ['Malantonio, Anna'], + rights_statement_ssim: ['http://creativecommons.org/publicdomain/mark/1.0/'], + rights_statement_label_ssim: ['Public Domain Mark'], + rights_statement_shortcode_ssim: ['PDM'], + source_tesim: ['Lafayette College'], + source_sim: ['Lafayette College'], + source_identifier_ssim: ['test.1.1'], + subtitle_tesim: ['a curious work'], + subtitle_sim: ['a curious work'], + suppressed_bsi: false, # Hyrax-managed field + system_create_dtsi: nil, # Hyrax-managed field + system_modified_dtsi: nil, # Hyrax-managed field + thumbnail_path_ss: default_thumbnail_path, # Hyrax-managed field + title_sim: ['Title of Work'], + title_tesim: ['Title of Work'], + title_alternative_tesim: ['another name'], + title_alternative_sim: ['another name'], + visibility_ssi: 'restricted', # Hyrax-managed field + years_encompassed_iim: [2026] + }.with_indifferent_access) + end + # rubocop:enable Layout/FirstHashElementIndentation +end From d125656f4ed806e675f24a318f882351e0c790c3 Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Thu, 14 May 2026 17:03:16 -0400 Subject: [PATCH 06/38] can navigate to work forms --- .../hyrax/audio_visuals_controller.rb | 7 ++-- app/controllers/hyrax/images_controller.rb | 5 ++- .../hyrax/publications_controller.rb | 5 ++- .../hyrax/student_works_controller.rb | 5 ++- app/forms/publication_resource_form.rb | 7 ++++ app/models/ability.rb | 8 ++-- app/models/admin_set_resource.rb | 9 ++++ app/models/audio_visual_resource.rb | 2 + app/models/collection_resource.rb | 6 +++ app/models/image_resource.rb | 2 + app/models/publication_resource.rb | 8 ++-- app/models/student_work_resource.rb | 2 + .../shared/_select_work_type_modal.html.erb | 42 +++++++++++++++++++ config/application.rb | 24 +++++++++++ config/initializers/hyrax.rb | 15 +++++-- config/initializers/spot_overrides.rb | 2 - config/initializers/wings.rb | 30 ++++++------- db/seeds.rb | 18 ++++++-- db/structure.sql | 4 +- 19 files changed, 163 insertions(+), 38 deletions(-) create mode 100644 app/forms/publication_resource_form.rb create mode 100644 app/models/admin_set_resource.rb create mode 100644 app/models/collection_resource.rb create mode 100644 app/views/shared/_select_work_type_modal.html.erb diff --git a/app/controllers/hyrax/audio_visuals_controller.rb b/app/controllers/hyrax/audio_visuals_controller.rb index 94f74f22c..146f5e156 100644 --- a/app/controllers/hyrax/audio_visuals_controller.rb +++ b/app/controllers/hyrax/audio_visuals_controller.rb @@ -3,9 +3,10 @@ module Hyrax class AudioVisualsController < ApplicationController include ::Spot::WorksControllerBehavior - # @todo for valkyrization - # self.curation_concern_type = Hyrax.config.use_valkyrie? ? AudioVisualResource : AudioVisual - self.curation_concern_type = ::AudioVisual + # self.curation_concern_type = ::AudioVisual + self.curation_concern_type = ::PublicationResource + self.work_form_service = Hyrax::FormFactory.new + self.show_presenter = Hyrax::AudioVisualPresenter end end diff --git a/app/controllers/hyrax/images_controller.rb b/app/controllers/hyrax/images_controller.rb index aae3386fd..3a34b09c5 100644 --- a/app/controllers/hyrax/images_controller.rb +++ b/app/controllers/hyrax/images_controller.rb @@ -3,7 +3,10 @@ module Hyrax class ImagesController < ApplicationController include ::Spot::WorksControllerBehavior - self.curation_concern_type = ::Image + # self.curation_concern_type = ::Image + self.curation_concern_type = ::PublicationResource + self.work_form_service = Hyrax::FormFactory.new + self.show_presenter = Hyrax::ImagePresenter end end diff --git a/app/controllers/hyrax/publications_controller.rb b/app/controllers/hyrax/publications_controller.rb index 0eed8a5b8..f195b7c8c 100644 --- a/app/controllers/hyrax/publications_controller.rb +++ b/app/controllers/hyrax/publications_controller.rb @@ -3,7 +3,10 @@ module Hyrax class PublicationsController < ApplicationController include Spot::WorksControllerBehavior - self.curation_concern_type = ::Publication + # self.curation_concern_type = ::Publication + self.curation_concern_type = ::PublicationResource + self.work_form_service = Hyrax::FormFactory.new + self.show_presenter = Hyrax::PublicationPresenter end end diff --git a/app/controllers/hyrax/student_works_controller.rb b/app/controllers/hyrax/student_works_controller.rb index 361f4a906..27c0de19a 100644 --- a/app/controllers/hyrax/student_works_controller.rb +++ b/app/controllers/hyrax/student_works_controller.rb @@ -3,7 +3,10 @@ module Hyrax class StudentWorksController < ApplicationController include Spot::WorksControllerBehavior - self.curation_concern_type = ::StudentWork + # self.curation_concern_type = ::StudentWork + self.curation_concern_type = ::PublicationResource + self.work_form_service = Hyrax::FormFactory.new + self.show_presenter = Hyrax::StudentWorkPresenter # Modifying the search_builder_class to our subclass which allows diff --git a/app/forms/publication_resource_form.rb b/app/forms/publication_resource_form.rb new file mode 100644 index 000000000..183726ae5 --- /dev/null +++ b/app/forms/publication_resource_form.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +class PublicationResourceForm < Hyrax::Forms::ResourceForm(PublicationResource) + include Hyrax::FormFields(:core_metadata) + include Hyrax::FormFields(:base_metadata) + include Hyrax::FormFields(:publication_metadata) + include Hyrax::FormFields(:institutional_metadata) +end diff --git a/app/models/ability.rb b/app/models/ability.rb index cf01b3b07..803a6d298 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -61,7 +61,7 @@ def admin_abilities def authenticated_users_can_deposit_student_works return unless registered_user? - can(:create, StudentWork) + can(:create, StudentWorkResource) end # Delegates abilities for users that have the 'depositor' role @@ -71,13 +71,13 @@ def authenticated_users_can_deposit_student_works def depositor_abilities return unless current_user.depositor? - can(:create, Publication) + can(:create, PublicationResource) # can view the user dashboard can(:read, :dashboard) # can add items to collections - can(:deposit, Collection) + can(:deposit, CollectionResource) end # Delegates abilities for users that have the 'student' role @@ -95,7 +95,7 @@ def faculty_abilities def student_abilities return unless current_user.student? - can(:create, StudentWork) + can(:create, StudentWorkResource) can(:read, :dashboard) end diff --git a/app/models/admin_set_resource.rb b/app/models/admin_set_resource.rb new file mode 100644 index 000000000..f86144ab6 --- /dev/null +++ b/app/models/admin_set_resource.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +# +# Subclassing our own AdminSetResource model to give a path to modify down the road +class AdminSetResource < Hyrax::AdministrativeSet + include Hyrax::ArResource + include Hyrax::Permissions::Readable + + Hyrax::ValkyrieLazyMigration.migrating(self, from: ::AdminSet) +end diff --git a/app/models/audio_visual_resource.rb b/app/models/audio_visual_resource.rb index 3cf3507f3..2aa1cacca 100644 --- a/app/models/audio_visual_resource.rb +++ b/app/models/audio_visual_resource.rb @@ -4,4 +4,6 @@ class AudioVisualResource < ::Hyrax::Work include Hyrax::Schema(:audio_visual_metadata, schema_loader: Spot::SimpleSchemaLoader.new) attribute :stored_derivatives, Valkyrie::Types::String + + Hyrax::ValkyrieLazyMigration.migrating(self, from: ::AudioVisual) end diff --git a/app/models/collection_resource.rb b/app/models/collection_resource.rb new file mode 100644 index 000000000..71f087594 --- /dev/null +++ b/app/models/collection_resource.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +class CollectionResource < Hyrax::PcdmCollection + include Hyrax::Schema(:core_metadata) + + Hyrax::ValkyrieLazyMigration.migrating(self, from: ::Collection) +end diff --git a/app/models/image_resource.rb b/app/models/image_resource.rb index 9de64fee0..4c9100227 100644 --- a/app/models/image_resource.rb +++ b/app/models/image_resource.rb @@ -2,4 +2,6 @@ class ImageResource < ::Hyrax::Work include Hyrax::Schema(:base_metadata, schema_loader: Spot::SimpleSchemaLoader.new) include Hyrax::Schema(:image_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + + Hyrax::ValkyrieLazyMigration.migrating(self, from: ::Image) end diff --git a/app/models/publication_resource.rb b/app/models/publication_resource.rb index 5900482b7..72a8e5fd3 100644 --- a/app/models/publication_resource.rb +++ b/app/models/publication_resource.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class PublicationResource < ::Hyrax::Work - include Hyrax::Schema(:base_metadata, schema_loader: Spot::SimpleSchemaLoader.new) - include Hyrax::Schema(:institutional_metadata, schema_loader: Spot::SimpleSchemaLoader.new) - include Hyrax::Schema(:publication_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + include Hyrax::Schema(:base_metadata) #, schema_loader: Spot::SimpleSchemaLoader.new) + include Hyrax::Schema(:institutional_metadata) #, schema_loader: Spot::SimpleSchemaLoader.new) + include Hyrax::Schema(:publication_metadata) #, schema_loader: Spot::SimpleSchemaLoader.new) + + Hyrax::ValkyrieLazyMigration.migrating(self, from: ::Publication) end diff --git a/app/models/student_work_resource.rb b/app/models/student_work_resource.rb index 316197ce9..842fa76c4 100644 --- a/app/models/student_work_resource.rb +++ b/app/models/student_work_resource.rb @@ -3,4 +3,6 @@ class StudentWorkResource < ::Hyrax::Work include Hyrax::Schema(:base_metadata, schema_loader: Spot::SimpleSchemaLoader.new) include Hyrax::Schema(:institutional_metadata, schema_loader: Spot::SimpleSchemaLoader.new) include Hyrax::Schema(:student_work_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + + Hyrax::ValkyrieLazyMigration.migrating(self, from: ::StudentWork) end diff --git a/app/views/shared/_select_work_type_modal.html.erb b/app/views/shared/_select_work_type_modal.html.erb new file mode 100644 index 000000000..bd48c05c7 --- /dev/null +++ b/app/views/shared/_select_work_type_modal.html.erb @@ -0,0 +1,42 @@ +<% # TODO: This should not live in views/shared. It does not need to be included on every page. %> + \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index 9a64b2196..c8b47f962 100644 --- a/config/application.rb +++ b/config/application.rb @@ -24,6 +24,7 @@ module Spot class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 6.1 + config.add_autoload_paths_to_load_path = true # use sidekiq for async jobs config.active_job.queue_adapter = :sidekiq @@ -50,5 +51,28 @@ class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. + + ## + # When using the Goddess adapter of Hyrax 5.x, we want to have a + # canonical answer for what are the Work Types that we want to manage. + # + # We don't want to rely on `Hyrax.config.curation_concerns`, as these are + # the ActiveFedora implementations. + # + # @return [Array] + def self.work_types + Hyrax.config.curation_concerns.map do |cc| + if cc.to_s.end_with?("Resource") + cc + else + # We may encounter a case where we don't have an old ActiveFedora + # model that we're mapping to. For example, let's say we add Game as + # a curation concern. And Game has only ever been written/modeled via + # Valkyrie. We don't want to also have a GameResource. + "#{cc}Resource".safe_constantize || cc + end + end + end + end end diff --git a/config/initializers/hyrax.rb b/config/initializers/hyrax.rb index 1ec742121..6f4900763 100644 --- a/config/initializers/hyrax.rb +++ b/config/initializers/hyrax.rb @@ -2,14 +2,21 @@ require 'wings' Hyrax.config do |config| - config.register_curation_concern :publication, :image, :student_work, :audio_visual + # Dassie seems to be explicitly _not_ registering the _resource concern, so maybe we shouldn't either? + %i[publication image student_work audio_visual].each do |type| + # config.register_curation_concern :"#{type}_resource" + config.register_curation_concern type + end # Can't define this within the Bulkrax initializer as it runs _before_ this Bulkrax.default_work_type = Hyrax.config.curation_concerns.first.name - config.admin_set_model = '::AdminSet' - config.collection_model = '::Collection' - config.file_set_model = '::FileSet' + # config.admin_set_model = '::AdminSet' + # config.collection_model = '::Collection' + # config.file_set_model = '::FileSet' + config.admin_set_model = 'AdminSetResource' + config.collection_model = 'CollectionResource' + config.file_set_model = 'Hyrax::FileSet' config.solr_default_method = :post diff --git a/config/initializers/spot_overrides.rb b/config/initializers/spot_overrides.rb index 6e135c407..ebf58c6b9 100644 --- a/config/initializers/spot_overrides.rb +++ b/config/initializers/spot_overrides.rb @@ -168,8 +168,6 @@ def add_sorting_to_solr(solr_parameters) Hyrax::CollectionMemberSearchBuilder.prepend(Spot::CollectionMemberSearchBuilderDecorator) - # Hyrax::AdminSetCreateService.singleton_class.send(:prepend, Spot::AdminSetCreateServiceDecorator) - # Only store entitlements related to us in the session to prevent a cookie overflow. # # @see https://github.com/biola/rack-cas/blob/v0.16.1/lib/rack/cas.rb#L96-L102 diff --git a/config/initializers/wings.rb b/config/initializers/wings.rb index b64d6a2e4..e541b7755 100644 --- a/config/initializers/wings.rb +++ b/config/initializers/wings.rb @@ -2,8 +2,6 @@ # # Set up cribbed from the Dassie example app within Hyrax, which itself is adapted from Hyku. # -# @note I have VALKYRIE_TRANSITION=true defined in .env.local but I'm unsure if it's neccessary? -# @see https://github.com/samvera/hyrax/blob/hyrax-v5.2.0/.dassie/config/initializers/wings.rb Rails.application.config.after_initialize do # active_fedora models we're migrating [Publication, Image, StudentWork, AudioVisual].each do |work_type| @@ -14,14 +12,20 @@ Wings::ModelRegistry.register(work_type, work_type) end - Wings::ModelRegistry.register(Collection, Collection) + # Map AdminSets and Collections + Wings::ModelRegistry.register(AdminSetResource, AdminSet) Wings::ModelRegistry.register(AdminSet, AdminSet) - Wings::ModelRegistry.register(FileSet, FileSet) + Wings::ModelRegistry.register(CollectionResource, Collection) + Wings::ModelRegistry.register(Collection, Collection) + Wings::ModelRegistry.register(Hyrax::FileSet, FileSet) - Wings::ModelRegistry.register(Hydra::PCDM::File, Hydra::PCDM::File) + Wings::ModelRegistry.register(FileSet, FileSet) + Wings::ModelRegistry.register(Hyrax::FileMetadata, Hydra::PCDM::File) + Wings::ModelRegistry.register(Hydra::PCDM::File, Hydra::PCDM::File) Valkyrie::MetadataAdapter.register(Freyja::MetadataAdapter.new, :freyja) + Valkyrie.config.metadata_adapter = :freyja Valkyrie::StorageAdapter.register( Valkyrie::Storage::VersionedDisk.new( @@ -30,12 +34,10 @@ ), :disk ) + Valkyrie.config.storage_adapter = :disk Hyrax.config.query_index_from_valkyrie = true Hyrax.config.index_adapter = :solr_index - - Valkyrie.config.metadata_adapter = :freyja - Valkyrie.config.storage_adapter = :disk Valkyrie.config.indexing_adapter = :solr_index # load all the sql based custom queries @@ -61,13 +63,13 @@ end Rails.application.config.to_prepare do - # AdminSetResource.class_eval do - # attribute :internal_resource, Valkyrie::Types::Any.default("AdminSet"), internal: true - # end + AdminSetResource.class_eval do + attribute :internal_resource, Valkyrie::Types::Any.default('AdminSet'), internal: true + end - # CollectionResource.class_eval do - # attribute :internal_resource, Valkyrie::Types::Any.default("Collection"), internal: true - # end + CollectionResource.class_eval do + attribute :internal_resource, Valkyrie::Types::Any.default('Collection'), internal: true + end Valkyrie.config.resource_class_resolver = lambda do |resource_klass_name| # TODO: Can we use some kind of lookup. diff --git a/db/seeds.rb b/db/seeds.rb index 6fe22b1d7..9c8ebc77f 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -13,11 +13,23 @@ errors = Hyrax::Workflow::WorkflowImporter.load_errors abort("Failed to process all workflows:\n #{errors.join('\n ')}") unless errors.empty? +# I think Hyrax::AdminSetCreateService.find_or_create_default_admin_set needs to be updated +# to use a check for :use_valkyrie in place of :disable_wings, because the Freyja adapter +# depends on Wings' conversion infrastructure. This does the work of the private method :create_default_admin_set! +# +# @see https://github.com/samvera/hyrax/blob/hyrax-v5.2.0/app/services/hyrax/admin_set_create_service.rb#L71-L78 puts "\n== Creating default admin set" -admin_set_id = Hyrax::AdminSetCreateService.find_or_create_default_admin_set.id.to_s +begin + Hyrax::AdminSetCreateService.find_or_create_default_admin_set +rescue Valkyrie::Persistence::UnsupportedDatatype + admin_set = AdminSetResource.new(title: ['Default Admin Set'], alternate_ids: ['admin_set/default', 'admin_set_default']) + admin_set = Hyrax::AdminSetCreateService.new(admin_set: admin_set, creating_user: nil, default_admin_set: true).create! + Hyrax::AdminSetCreateService.default_admin_set_persister.update(default_admin_set_id: admin_set.id) +end + -puts "\n== Ensuring the found or created admin set is indexed" -AdminSet.find(admin_set_id).update_index +# puts "\n== Ensuring the found or created admin set is indexed" +# AdminSet.find(admin_set_id).update_index # Legacy seeding done by Rake tasks # @see lib/tasks/spot diff --git a/db/structure.sql b/db/structure.sql index 225c82fbf..cf7173ccf 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1,4 +1,4 @@ -\restrict 3tq1NbQsJWPHVS4qUFpTdLUpqfZeRSkN6zTmZeAcPmrO9aozielEKOgJcAmVCRD +\restrict lx2vxHyypdaSFHgcml7gnYOwJSn6kWDgeI4fkIiDH4YbMpXFTHeLTbbUiVFAd06 -- Dumped from database version 15.17 -- Dumped by pg_dump version 15.16 (Debian 15.16-0+deb12u1) @@ -3937,7 +3937,7 @@ ALTER TABLE ONLY public.mailboxer_receipts -- PostgreSQL database dump complete -- -\unrestrict 3tq1NbQsJWPHVS4qUFpTdLUpqfZeRSkN6zTmZeAcPmrO9aozielEKOgJcAmVCRD +\unrestrict lx2vxHyypdaSFHgcml7gnYOwJSn6kWDgeI4fkIiDH4YbMpXFTHeLTbbUiVFAd06 SET search_path TO "$user", public; From 742d28638ddfef70f34a3ab171e59618685966e2 Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Mon, 18 May 2026 10:42:00 -0400 Subject: [PATCH 07/38] wip first pass @ forms --- .../spot/forms/resource_form_helpers.rb | 123 ++++++++++++++++++ app/forms/publication_resource_form.rb | 5 + app/models/audio_visual_resource.rb | 3 +- app/models/base_resource.rb | 12 ++ app/models/collection.rb | 2 +- app/models/concerns/spot/core_metadata.rb | 2 +- .../concerns/spot/has_controlled_fields.rb | 21 +++ app/models/image_resource.rb | 3 +- app/models/publication_resource.rb | 7 +- .../{location.rb => geonames_location.rb} | 2 +- app/models/student_work_resource.rb | 3 +- app/services/rdf_literal_serializer.rb | 9 +- 12 files changed, 172 insertions(+), 20 deletions(-) create mode 100644 app/forms/concerns/spot/forms/resource_form_helpers.rb create mode 100644 app/models/base_resource.rb create mode 100644 app/models/concerns/spot/has_controlled_fields.rb rename app/models/spot/controlled_vocabularies/{location.rb => geonames_location.rb} (98%) diff --git a/app/forms/concerns/spot/forms/resource_form_helpers.rb b/app/forms/concerns/spot/forms/resource_form_helpers.rb new file mode 100644 index 000000000..321c42840 --- /dev/null +++ b/app/forms/concerns/spot/forms/resource_form_helpers.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true +module Spot + module Forms + # Helper methods to add virtual fields for handling form fields + module ResourceFormHelpers + extend ActiveSupport::Concern + + module ClassMethods + # Adds a _attributes virtual property to the form which + # is used for Select2 typeahead dropdowns. This maps URI values + # + # @example + # resource.subject + # => ['https://ldr.lafayette.edu'] + # + # form = Hyrax::ResourceForm.for(resource: resource) + # form.subject + # => ['https://ldr.lafayette.edu'] + # form.subject_attributes + # => { '0' => 'https://ldr.lafayette.edu' } + def nested_attributes_for(*fields) + fields.each do |field| + property(:"#{field}_attributes", + virtual: true, + prepopulator: nested_attribute_prepopulator_for(field), + populator: nested_attribute_populator_for(field)) + end + end + + # We store certain fields tagged + def language_tagged_field(*fields) + fields.each do |field| + property(:"#{field}_value", + virtual: true, + prepopulator: language_value_prepopulator_for(field), + populator: language_value_populator_for(field)) + property(:"#{field}_language", + virtual: true, + prepopulator: language_prepopulator_for(field)) + end + end + + private + + def language_value_populator_for(field) + lambda do |doc:, **| + values = Array.wrap(doc["#{field}_value"]) + .zip(Array.wrap(doc["#{field}_language"])) + .map { |(value, language)| rdf_literal_from(value, language) } + .compact + .map { |literal| rdf_serializer.serialize(literal) } + + send(:"#{field}=", values) + end + end + + def language_value_prepopulator_for(field) + lambda do + values = Array.wrap(send(field)).map do |value| + rdf_serializer.deserialize(value)&.value || value + end + + send(:"#{field}_value=", values) + end + end + + def language_prepopulator_for(field) + lambda do + languages = Array.wrap(send(field)).map do |value| + rdf_serializer.deserialize(value).language + end + + send(:"#{field}_language=", languages) + end + end + + def nested_attribute_populator_for(field, value_key: 'id', destroy_key: '_destroy') + lambda do |*, fragment:| + adds = [] + deletes = [] + + fragment.each do |_idx, attrs| + value = attrs[value_key] + if attrs[destroy_key] == 'true' + deletes << value + else + adds << value + end + end + + merged_values = ((Array.wrap(send(field)).map(&:to_s) + adds) - deletes).uniq + send(:"#{field}=", merged_values) + end + end + + def nested_attribute_prepopulator_for(field, value_key: 'id') + lambda do + attributes = Array.wrap(send(field)) + .each_with_object({}) do |value, attrs| + attrs[attrs.size.to_s] = { value_key => value.to_s } + end + + # @todo do we need an empty value to show a new form line? + attributes[attributes.size.to_s] = { 'id' => '' } + + send(:"#{field}_attributes=", attributes) + end + end + + def rdf_literal_from(value, language) + return if value.blank? + return RDF::Literal.new(value.to_s) if language.blank? + + RDF::Literal.new(value.to_s, language: language.to_sym) + end + + def rdf_serializer + @rdf_serializer ||= RdfLiteralSerializer.new + end + end + end + end +end diff --git a/app/forms/publication_resource_form.rb b/app/forms/publication_resource_form.rb index 183726ae5..69423a97e 100644 --- a/app/forms/publication_resource_form.rb +++ b/app/forms/publication_resource_form.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true class PublicationResourceForm < Hyrax::Forms::ResourceForm(PublicationResource) + include Spot::Forms::ResourceFormHelpers + include Hyrax::FormFields(:core_metadata) include Hyrax::FormFields(:base_metadata) include Hyrax::FormFields(:publication_metadata) include Hyrax::FormFields(:institutional_metadata) + + language_tagged_field(:title, :title_alternative, :subtitle, :abstract, :description) + nested_attributes_for(:subject, :location, :language, :academic_department, :division) end diff --git a/app/models/audio_visual_resource.rb b/app/models/audio_visual_resource.rb index 2aa1cacca..3d9ef9383 100644 --- a/app/models/audio_visual_resource.rb +++ b/app/models/audio_visual_resource.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -class AudioVisualResource < ::Hyrax::Work - include Hyrax::Schema(:base_metadata, schema_loader: Spot::SimpleSchemaLoader.new) +class AudioVisualResource < BaseResource include Hyrax::Schema(:audio_visual_metadata, schema_loader: Spot::SimpleSchemaLoader.new) attribute :stored_derivatives, Valkyrie::Types::String diff --git a/app/models/base_resource.rb b/app/models/base_resource.rb new file mode 100644 index 000000000..eab40fef8 --- /dev/null +++ b/app/models/base_resource.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +# +# Common fields / behaviors shared across Work types +class BaseResource < Hyrax::Work + include Spot::HasControlledFields + + include Hyrax::Schema(:core_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + include Hyrax::Schema(:base_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + + has_controlled_field :subject, vocabulary_class: Spot::ControlledVocabularies::AssignFastSubject + has_controlled_field :location, vocabulary_class: Spot::ControlledVocabularies::GeonamesLocation +end diff --git a/app/models/collection.rb b/app/models/collection.rb index 2cf260d07..a64ed2466 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -38,7 +38,7 @@ class Collection < ActiveFedora::Base end property :location, predicate: ::RDF::Vocab::DC.spatial, - class_name: Spot::ControlledVocabularies::Location do |index| + class_name: Spot::ControlledVocabularies::GeonamesLocation do |index| index.as :symbol end diff --git a/app/models/concerns/spot/core_metadata.rb b/app/models/concerns/spot/core_metadata.rb index 660303f04..6c66fff0a 100644 --- a/app/models/concerns/spot/core_metadata.rb +++ b/app/models/concerns/spot/core_metadata.rb @@ -52,7 +52,7 @@ module CoreMetadata # # @see {Spot::DeepIndexingService} for label indexing details property :location, predicate: ::RDF::Vocab::DC.spatial, - class_name: Spot::ControlledVocabularies::Location do |index| + class_name: Spot::ControlledVocabularies::GeonamesLocation do |index| index.as :symbol end diff --git a/app/models/concerns/spot/has_controlled_fields.rb b/app/models/concerns/spot/has_controlled_fields.rb new file mode 100644 index 000000000..fe59c48e7 --- /dev/null +++ b/app/models/concerns/spot/has_controlled_fields.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +module Spot + # Opt-in wrapping of controlled vocabulary field values in an ActiveTriples::Resource object + module HasControlledFields + extend ActiveSupport::Concern + + module ClassMethods + def has_controlled_field(field, vocabulary_class: ActiveTriples::Resource) + controlled_fields << field unless controlled_fields.include?(field) + + define_method(field.to_sym) do + Array.wrap(try(:[], field.to_sym)).map { |v| vocabulary_class.new(v) } || nil + end + end + + def controlled_fields + @spot_controlled_fields ||= [] + end + end + end +end diff --git a/app/models/image_resource.rb b/app/models/image_resource.rb index 4c9100227..2e5125131 100644 --- a/app/models/image_resource.rb +++ b/app/models/image_resource.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -class ImageResource < ::Hyrax::Work - include Hyrax::Schema(:base_metadata, schema_loader: Spot::SimpleSchemaLoader.new) +class ImageResource < BaseResource include Hyrax::Schema(:image_metadata, schema_loader: Spot::SimpleSchemaLoader.new) Hyrax::ValkyrieLazyMigration.migrating(self, from: ::Image) diff --git a/app/models/publication_resource.rb b/app/models/publication_resource.rb index 72a8e5fd3..80ed3ac57 100644 --- a/app/models/publication_resource.rb +++ b/app/models/publication_resource.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true -class PublicationResource < ::Hyrax::Work - include Hyrax::Schema(:base_metadata) #, schema_loader: Spot::SimpleSchemaLoader.new) - include Hyrax::Schema(:institutional_metadata) #, schema_loader: Spot::SimpleSchemaLoader.new) - include Hyrax::Schema(:publication_metadata) #, schema_loader: Spot::SimpleSchemaLoader.new) +class PublicationResource < BaseResource + include Hyrax::Schema(:institutional_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + include Hyrax::Schema(:publication_metadata, schema_loader: Spot::SimpleSchemaLoader.new) Hyrax::ValkyrieLazyMigration.migrating(self, from: ::Publication) end diff --git a/app/models/spot/controlled_vocabularies/location.rb b/app/models/spot/controlled_vocabularies/geonames_location.rb similarity index 98% rename from app/models/spot/controlled_vocabularies/location.rb rename to app/models/spot/controlled_vocabularies/geonames_location.rb index 2b96c2b29..48fb6e6a7 100644 --- a/app/models/spot/controlled_vocabularies/location.rb +++ b/app/models/spot/controlled_vocabularies/geonames_location.rb @@ -10,7 +10,7 @@ # data is being pulled from the same source as the RDF data, it seems # Okay to store the API label value. module Spot::ControlledVocabularies - class Location < Base + class GeonamesLocation < Base # Now that we're caching label values, this is not called unless # the resource's label matches the RDF subject. As part of the # preferred_label check, we call {#pick_preferred_label} which, diff --git a/app/models/student_work_resource.rb b/app/models/student_work_resource.rb index 842fa76c4..9313bf9bd 100644 --- a/app/models/student_work_resource.rb +++ b/app/models/student_work_resource.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -class StudentWorkResource < ::Hyrax::Work - include Hyrax::Schema(:base_metadata, schema_loader: Spot::SimpleSchemaLoader.new) +class StudentWorkResource < BaseResource include Hyrax::Schema(:institutional_metadata, schema_loader: Spot::SimpleSchemaLoader.new) include Hyrax::Schema(:student_work_metadata, schema_loader: Spot::SimpleSchemaLoader.new) diff --git a/app/services/rdf_literal_serializer.rb b/app/services/rdf_literal_serializer.rb index 4ddcc6370..63c97e63f 100644 --- a/app/services/rdf_literal_serializer.rb +++ b/app/services/rdf_literal_serializer.rb @@ -12,18 +12,13 @@ def deserialize(string) private - # @return [Symbol] - def type - :ntriples - end - # @return [RDF::Reader] def reader - @reader ||= RDF::Reader.for(type) + @reader ||= RDF::Reader.for(:ntriples) end # @return [RDF::Writer] def writer - @writer ||= RDF::Writer.for(type).new + @writer ||= RDF::Writer.for(:ntriples).new end end From d2945ba37d8fadb781abfc5cc0604f966672cf94 Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Mon, 18 May 2026 14:39:10 -0400 Subject: [PATCH 08/38] we don't need to serialize rdf literals for valkyrie --- app/forms/concerns/spot/forms/resource_form_helpers.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/forms/concerns/spot/forms/resource_form_helpers.rb b/app/forms/concerns/spot/forms/resource_form_helpers.rb index 321c42840..fec2da4a9 100644 --- a/app/forms/concerns/spot/forms/resource_form_helpers.rb +++ b/app/forms/concerns/spot/forms/resource_form_helpers.rb @@ -7,7 +7,8 @@ module ResourceFormHelpers module ClassMethods # Adds a _attributes virtual property to the form which - # is used for Select2 typeahead dropdowns. This maps URI values + # is used for Select2 typeahead dropdowns. When sync'd with the + # resource, it converts this form into # # @example # resource.subject @@ -27,7 +28,6 @@ def nested_attributes_for(*fields) end end - # We store certain fields tagged def language_tagged_field(*fields) fields.each do |field| property(:"#{field}_value", @@ -48,7 +48,6 @@ def language_value_populator_for(field) .zip(Array.wrap(doc["#{field}_language"])) .map { |(value, language)| rdf_literal_from(value, language) } .compact - .map { |literal| rdf_serializer.serialize(literal) } send(:"#{field}=", values) end From f16ee07ac819ebbc84108b39eb8a1e3e1cc3029c Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Thu, 21 May 2026 16:08:06 -0400 Subject: [PATCH 09/38] wip valkyrie postgres writes work? --- .../spot/forms/resource_form_helpers.rb | 122 ------------------ app/forms/publication_resource_form.rb | 41 +++++- app/models/admin_set_resource.rb | 2 +- app/models/audio_visual_resource.rb | 2 - app/models/collection_resource.rb | 2 +- app/models/image_resource.rb | 2 - app/models/publication_resource.rb | 12 +- app/models/spot/identifier.rb | 1 + app/models/student_work_resource.rb | 2 - config/initializers/hyrax.rb | 11 -- config/initializers/wings.rb | 30 +++-- config/metadata/core_metadata.yaml | 1 + 12 files changed, 68 insertions(+), 160 deletions(-) delete mode 100644 app/forms/concerns/spot/forms/resource_form_helpers.rb diff --git a/app/forms/concerns/spot/forms/resource_form_helpers.rb b/app/forms/concerns/spot/forms/resource_form_helpers.rb deleted file mode 100644 index fec2da4a9..000000000 --- a/app/forms/concerns/spot/forms/resource_form_helpers.rb +++ /dev/null @@ -1,122 +0,0 @@ -# frozen_string_literal: true -module Spot - module Forms - # Helper methods to add virtual fields for handling form fields - module ResourceFormHelpers - extend ActiveSupport::Concern - - module ClassMethods - # Adds a _attributes virtual property to the form which - # is used for Select2 typeahead dropdowns. When sync'd with the - # resource, it converts this form into - # - # @example - # resource.subject - # => ['https://ldr.lafayette.edu'] - # - # form = Hyrax::ResourceForm.for(resource: resource) - # form.subject - # => ['https://ldr.lafayette.edu'] - # form.subject_attributes - # => { '0' => 'https://ldr.lafayette.edu' } - def nested_attributes_for(*fields) - fields.each do |field| - property(:"#{field}_attributes", - virtual: true, - prepopulator: nested_attribute_prepopulator_for(field), - populator: nested_attribute_populator_for(field)) - end - end - - def language_tagged_field(*fields) - fields.each do |field| - property(:"#{field}_value", - virtual: true, - prepopulator: language_value_prepopulator_for(field), - populator: language_value_populator_for(field)) - property(:"#{field}_language", - virtual: true, - prepopulator: language_prepopulator_for(field)) - end - end - - private - - def language_value_populator_for(field) - lambda do |doc:, **| - values = Array.wrap(doc["#{field}_value"]) - .zip(Array.wrap(doc["#{field}_language"])) - .map { |(value, language)| rdf_literal_from(value, language) } - .compact - - send(:"#{field}=", values) - end - end - - def language_value_prepopulator_for(field) - lambda do - values = Array.wrap(send(field)).map do |value| - rdf_serializer.deserialize(value)&.value || value - end - - send(:"#{field}_value=", values) - end - end - - def language_prepopulator_for(field) - lambda do - languages = Array.wrap(send(field)).map do |value| - rdf_serializer.deserialize(value).language - end - - send(:"#{field}_language=", languages) - end - end - - def nested_attribute_populator_for(field, value_key: 'id', destroy_key: '_destroy') - lambda do |*, fragment:| - adds = [] - deletes = [] - - fragment.each do |_idx, attrs| - value = attrs[value_key] - if attrs[destroy_key] == 'true' - deletes << value - else - adds << value - end - end - - merged_values = ((Array.wrap(send(field)).map(&:to_s) + adds) - deletes).uniq - send(:"#{field}=", merged_values) - end - end - - def nested_attribute_prepopulator_for(field, value_key: 'id') - lambda do - attributes = Array.wrap(send(field)) - .each_with_object({}) do |value, attrs| - attrs[attrs.size.to_s] = { value_key => value.to_s } - end - - # @todo do we need an empty value to show a new form line? - attributes[attributes.size.to_s] = { 'id' => '' } - - send(:"#{field}_attributes=", attributes) - end - end - - def rdf_literal_from(value, language) - return if value.blank? - return RDF::Literal.new(value.to_s) if language.blank? - - RDF::Literal.new(value.to_s, language: language.to_sym) - end - - def rdf_serializer - @rdf_serializer ||= RdfLiteralSerializer.new - end - end - end - end -end diff --git a/app/forms/publication_resource_form.rb b/app/forms/publication_resource_form.rb index 69423a97e..91fbc0ebd 100644 --- a/app/forms/publication_resource_form.rb +++ b/app/forms/publication_resource_form.rb @@ -1,12 +1,43 @@ # frozen_string_literal: true class PublicationResourceForm < Hyrax::Forms::ResourceForm(PublicationResource) - include Spot::Forms::ResourceFormHelpers + include Spot::Forms::BaseResourceFormBehavior - include Hyrax::FormFields(:core_metadata) - include Hyrax::FormFields(:base_metadata) include Hyrax::FormFields(:publication_metadata) include Hyrax::FormFields(:institutional_metadata) - language_tagged_field(:title, :title_alternative, :subtitle, :abstract, :description) - nested_attributes_for(:subject, :location, :language, :academic_department, :division) + nested_attributes_for(:academic_department, :division) + + def primary_terms # rubocop:disable Metrics/MethodLength + [ + :title, + :date_issued, + :resource_type, + :rights_statement, + + # starting with rights holder since it relates to rights_statement + :rights_holder, + :subtitle, + :title_alternative, + :creator, + :contributor, + :editor, + :publisher, + :source, + :bibliographic_citation, + :standard_identifier, + :local_identifier, + :abstract, + :description, + :subject, + :keyword, + :language, + :physical_medium, + :location, + :related_resource, + :academic_department, + :division, + :organization, + :note + ] + end end diff --git a/app/models/admin_set_resource.rb b/app/models/admin_set_resource.rb index f86144ab6..0a16a540f 100644 --- a/app/models/admin_set_resource.rb +++ b/app/models/admin_set_resource.rb @@ -5,5 +5,5 @@ class AdminSetResource < Hyrax::AdministrativeSet include Hyrax::ArResource include Hyrax::Permissions::Readable - Hyrax::ValkyrieLazyMigration.migrating(self, from: ::AdminSet) + attribute :internal_resource, Valkyrie::Types::Any.default('AdminSet'), internal: true end diff --git a/app/models/audio_visual_resource.rb b/app/models/audio_visual_resource.rb index 3d9ef9383..9d3861949 100644 --- a/app/models/audio_visual_resource.rb +++ b/app/models/audio_visual_resource.rb @@ -3,6 +3,4 @@ class AudioVisualResource < BaseResource include Hyrax::Schema(:audio_visual_metadata, schema_loader: Spot::SimpleSchemaLoader.new) attribute :stored_derivatives, Valkyrie::Types::String - - Hyrax::ValkyrieLazyMigration.migrating(self, from: ::AudioVisual) end diff --git a/app/models/collection_resource.rb b/app/models/collection_resource.rb index 71f087594..7aaf5592b 100644 --- a/app/models/collection_resource.rb +++ b/app/models/collection_resource.rb @@ -2,5 +2,5 @@ class CollectionResource < Hyrax::PcdmCollection include Hyrax::Schema(:core_metadata) - Hyrax::ValkyrieLazyMigration.migrating(self, from: ::Collection) + attribute :internal_resource, Valkyrie::Types::Any.default('Collection'), internal: true end diff --git a/app/models/image_resource.rb b/app/models/image_resource.rb index 2e5125131..6a3685043 100644 --- a/app/models/image_resource.rb +++ b/app/models/image_resource.rb @@ -1,6 +1,4 @@ # frozen_string_literal: true class ImageResource < BaseResource include Hyrax::Schema(:image_metadata, schema_loader: Spot::SimpleSchemaLoader.new) - - Hyrax::ValkyrieLazyMigration.migrating(self, from: ::Image) end diff --git a/app/models/publication_resource.rb b/app/models/publication_resource.rb index 80ed3ac57..e67270e55 100644 --- a/app/models/publication_resource.rb +++ b/app/models/publication_resource.rb @@ -3,5 +3,15 @@ class PublicationResource < BaseResource include Hyrax::Schema(:institutional_metadata, schema_loader: Spot::SimpleSchemaLoader.new) include Hyrax::Schema(:publication_metadata, schema_loader: Spot::SimpleSchemaLoader.new) - Hyrax::ValkyrieLazyMigration.migrating(self, from: ::Publication) + def identifier + attributes[:identifier].map { |v| v.is_a?(String) ? Spot::Identifier.from_string(v) : v } + end + + def local_identifier + identifier.select(&:local?) + end + + def standard_identifier + identifier.select(&:standard?) + end end diff --git a/app/models/spot/identifier.rb b/app/models/spot/identifier.rb index 1e76522d6..44b2e1556 100644 --- a/app/models/spot/identifier.rb +++ b/app/models/spot/identifier.rb @@ -49,6 +49,7 @@ class << self # @param [String] string_value # @return [Spot::Identifier] def from_string(string_value) + string_value = string_value.to_s unless string_value.is_a?(String) return new(nil, string_value) unless string_value.include?(SEPARATOR) prefix, id = string_value.split(SEPARATOR, 2) diff --git a/app/models/student_work_resource.rb b/app/models/student_work_resource.rb index 9313bf9bd..5cd1c7db2 100644 --- a/app/models/student_work_resource.rb +++ b/app/models/student_work_resource.rb @@ -2,6 +2,4 @@ class StudentWorkResource < BaseResource include Hyrax::Schema(:institutional_metadata, schema_loader: Spot::SimpleSchemaLoader.new) include Hyrax::Schema(:student_work_metadata, schema_loader: Spot::SimpleSchemaLoader.new) - - Hyrax::ValkyrieLazyMigration.migrating(self, from: ::StudentWork) end diff --git a/config/initializers/hyrax.rb b/config/initializers/hyrax.rb index 6f4900763..e92b118be 100644 --- a/config/initializers/hyrax.rb +++ b/config/initializers/hyrax.rb @@ -321,15 +321,4 @@ Rails.application.reloader.to_prepare do Date::DATE_FORMATS[:standard] = "%m/%d/%Y" - - # Hyrax v4 adds a helper method on the Hyrax constant that Bulkrax v9 depends on, - # so we'll patch it in if it doesn't exist yet. This came up while having issues - # with Bulkrax exports. - unless Hyrax.respond_to?(:index_field_mapper) - module Hyrax - def self.index_field_mapper - config.index_field_mapper - end - end - end end diff --git a/config/initializers/wings.rb b/config/initializers/wings.rb index e541b7755..11809025e 100644 --- a/config/initializers/wings.rb +++ b/config/initializers/wings.rb @@ -5,21 +5,25 @@ Rails.application.config.after_initialize do # active_fedora models we're migrating [Publication, Image, StudentWork, AudioVisual].each do |work_type| - Wings::ModelRegistry.register("#{work_type}Resource".constantize, work_type) + # Wings::ModelRegistry.register("#{work_type}Resource".constantize, work_type) # from dassie: # "we register itself so we can pre-translate the class in Freyja instead of having to translate in each query_service" - Wings::ModelRegistry.register(work_type, work_type) + # Wings::ModelRegistry.register(work_type, work_type) + + Hyrax::ValkyrieLazyMigration.migrating("#{work_type}Resource".constantize, from: work_type) end # Map AdminSets and Collections - Wings::ModelRegistry.register(AdminSetResource, AdminSet) - Wings::ModelRegistry.register(AdminSet, AdminSet) - Wings::ModelRegistry.register(CollectionResource, Collection) - Wings::ModelRegistry.register(Collection, Collection) + Hyrax::ValkyrieLazyMigration.migrating(AdminSetResource, from: AdminSet) + Hyrax::ValkyrieLazyMigration.migrating(CollectionResource, from: Collection) + + # Wings::ModelRegistry.register(AdminSet, AdminSet) + # Wings::ModelRegistry.register(Collection, Collection) + Wings::ModelRegistry.register(Hyrax::FileSet, FileSet) - Wings::ModelRegistry.register(FileSet, FileSet) + # Wings::ModelRegistry.register(FileSet, FileSet) Wings::ModelRegistry.register(Hyrax::FileMetadata, Hydra::PCDM::File) Wings::ModelRegistry.register(Hydra::PCDM::File, Hydra::PCDM::File) @@ -34,7 +38,7 @@ ), :disk ) - Valkyrie.config.storage_adapter = :disk + Valkyrie.config.storage_adapter = :disk Hyrax.config.query_index_from_valkyrie = true Hyrax.config.index_adapter = :solr_index @@ -64,20 +68,20 @@ Rails.application.config.to_prepare do AdminSetResource.class_eval do - attribute :internal_resource, Valkyrie::Types::Any.default('AdminSet'), internal: true + end CollectionResource.class_eval do attribute :internal_resource, Valkyrie::Types::Any.default('Collection'), internal: true end + # Copied from Valkyrie.config.resource_class_resolver = lambda do |resource_klass_name| - # TODO: Can we use some kind of lookup. klass_name = resource_klass_name.gsub(/^Wings\((.+)\)$/, '\1') klass_name = klass_name.gsub(/Resource$/, '') - if %w[ - GenericWork - ].include?(klass_name) + resource_types = Hyrax.config.curation_concerns.map(&:to_s).concat(['Collection', 'AdminSet']) + + if resource_types.include?(klass_name) "#{klass_name}Resource".constantize elsif 'Collection' == klass_name CollectionResource diff --git a/config/metadata/core_metadata.yaml b/config/metadata/core_metadata.yaml index 9af3d81a0..5e238b111 100644 --- a/config/metadata/core_metadata.yaml +++ b/config/metadata/core_metadata.yaml @@ -11,6 +11,7 @@ attributes: form: multiple: false required: true + primary: true date_modified: predicate: http://purl.org/dc/terms/modified type: date_time From 78e21b43beb9378c1912aa1c45e9e3ddddc9d81d Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Tue, 26 May 2026 10:05:17 -0400 Subject: [PATCH 10/38] forms submit but don't index? --- .../spot/forms/base_resource_form_behavior.rb | 78 ++++++++++++++++ .../spot/forms/language_tagged_fields.rb | 93 +++++++++++++++++++ .../concerns/spot/forms/nested_attributes.rb | 75 +++++++++++++++ app/indexers/base_resource_indexer.rb | 4 +- config/initializers/hyrax_events.rb | 4 +- config/initializers/wings.rb | 33 ++----- 6 files changed, 260 insertions(+), 27 deletions(-) create mode 100644 app/forms/concerns/spot/forms/base_resource_form_behavior.rb create mode 100644 app/forms/concerns/spot/forms/language_tagged_fields.rb create mode 100644 app/forms/concerns/spot/forms/nested_attributes.rb diff --git a/app/forms/concerns/spot/forms/base_resource_form_behavior.rb b/app/forms/concerns/spot/forms/base_resource_form_behavior.rb new file mode 100644 index 000000000..6fd37326f --- /dev/null +++ b/app/forms/concerns/spot/forms/base_resource_form_behavior.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true +module Spot + module Forms + # Since Hyrax 5 forms are inherited from a generated class, this seemed like the easiest way to inject common + # behavior into forms instead of using a BaseResourceForm class. + # + # Adds class method helpers: + # - `language_tagged_field(*fields)` for fields stored as RDF::Literals + # - `nested_attributes_for(*fields)` for fields using ControlledVocabularies (local and remote) + # + # In a bit of opinionated base behavior, this also: + # - sets up support for standard/local identifiers if the field :identifier is defined (included with `base_metadata.yml`) + # - sets up support for language tagged fields from base_metadata + # - :title + # - :title_alternative + # - :subtitle + # - :abstract + # - :description + # - sets up nested attribute support for controlled vocabulary fields from base_metadata + # - :subject + # - :location + # - :language + # + # @example + # class GoodResourceForm < Hyrax::Forms::ResourceForm(GoodResource) + # include Spot::Forms::BaseResourceFormBehaivor + # end + module BaseResourceFormBehavior + extend ActiveSupport::Concern + + included do + # helper method :language_tagged_field + include Spot::Forms::LanguageTaggedFields + + # helper method :nested_attributes_for + include Spot::Forms::NestedAttributes + + include Hyrax::FormFields(:core_metadata) + include Hyrax::FormFields(:base_metadata) + + # form = PublicationResourceForm.for(resource: PublicationResource.new(identifier:['abc:123'])) + + if model_class.attribute_names.include?(:identifier) + # for local_identifier, we exclude the noid: identifier so as to not let it be user-editable. + property :local_identifier, virtual: true, prepopulator: -> { self.local_identifier = model.local_identifier.reject { |id| id.try(:prefix) == 'noid' }.map(&:to_s) } + property :standard_identifier, virtual: true, prepopulator: -> { self.standard_identifier = model.standard_identifier.map(&:to_s) } + property :standard_identifier_prefix, virtual: true, prepopulator: -> { self.standard_identifier_prefix = model.standard_identifier.map(&:prefix) } + property :standard_identifier_value, virtual: true, prepopulator: -> { self.standard_identifier_value = model.standard_identifier.map(&:value) } + + validate :identifier do + send(:identifier=, merged_identifiers) + end + end + + [:title, :title_alternative, :subtitle, :abstract, :description].each do |field| + language_tagged_field(field) if model_class.attribute_names.include?(field) + end + + [:subject, :location, :language].each do |field| + nested_attributes_for(field) if model_class.attribute_names.include?(field) + end + end + + private + + def merged_identifiers + Array.wrap(standard_identifier_prefix.compact) + .zip(Array.wrap(standard_identifier_value.compact)) + .flat_map { |(prefix, id)| Spot::Identifier.new(prefix, id).to_s } + .concat(local_identifier.map(&:to_s)) + .concat(model.identifier.map(&:to_s)) + .flatten + .compact + .uniq + end + end + end +end diff --git a/app/forms/concerns/spot/forms/language_tagged_fields.rb b/app/forms/concerns/spot/forms/language_tagged_fields.rb new file mode 100644 index 000000000..540786e8e --- /dev/null +++ b/app/forms/concerns/spot/forms/language_tagged_fields.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true +module Spot + module Forms + module LanguageTaggedFields + extend ActiveSupport::Concern + + module ClassMethods + # Provides the option for a field's values to be tagged with a language. + # In the form, a field's values are mapped to a _value virtual property + # and any RDF language metadata is mapped to a _language virtual property. + # + # @example + # resource.title + # #=> ['the 400 Blows', RDF::Literal('Les quatres cents coups', language: :fr)] + # form = Hyrax::ResourceForm.for(resource: resource) + # form.title == resource.title + # #=> true + # form.title_value + # #=> ['the 400 Blows', 'Les quatres cents coups'] + # form.title_language + # #=> [nil, :fr] + def language_tagged_field(*fields) + fields.each do |field| + property(:"#{field}_value", + virtual: true, + prepopulator: language_value_prepopulator_for(field), + populator: language_value_populator_for(field)) + property(:"#{field}_language", + virtual: true, + prepopulator: language_prepopulator_for(field)) + end + end + + private + + # A lambda function to use for converting the _value and _language + # virtual fields into RDF::Literals stored in + def language_value_populator_for(field) + lambda do |doc:, **| + values = Array.wrap(doc["#{field}_value"]) + .zip(Array.wrap(doc["#{field}_language"])) + .map do |(value, language)| + next if value.blank? + next RDF::Literal(value) if language.blank? + RDF::Literal(value, language: language) + end.compact + + send(:"#{field}=", values) + end + end + + # A lambda function to map RDF::Literal values to their value attribute + # which prepopulates the _value virtual field. + def language_value_prepopulator_for(field) + lambda do + values = Array.wrap(model.send(field)).map do |value| + if value.is_a?(RDF::Literal) + rdf_serializer.deserialize(value)&.value + else + value.to_s + end + end + + send(:"#{field}_value=", values) + end + end + + # A lambda funciton to map RDF::Literal values to their :language attribute + # which prepopulates the the _language virtual property + def language_prepopulator_for(field) + lambda do + languages = Array.wrap(model.send(field)).map do |value| + rdf_serializer.deserialize(value)&.language + end + + send(:"#{field}_language=", languages) + end + end + + def rdf_literal_or_value_from(value, language) + return if value.blank? + return value if language.blank? + + RDF::Literal.new(value.to_s, language: language.to_sym) + end + + def rdf_serializer + @rdf_serializer ||= RdfLiteralSerializer.new + end + end + end + end +end diff --git a/app/forms/concerns/spot/forms/nested_attributes.rb b/app/forms/concerns/spot/forms/nested_attributes.rb new file mode 100644 index 000000000..f6534d64a --- /dev/null +++ b/app/forms/concerns/spot/forms/nested_attributes.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true +module Spot + module Forms + module NestedAttributes + extend ActiveSupport::Concern + + module ClassMethods + # Adds a _attributes virtual property to the form which + # is used for Select2 typeahead dropdowns. When sync'd with the + # resource, it converts this form into + # + # @example + # resource.subject + # => ['https://ldr.lafayette.edu'] + # + # form = Hyrax::ResourceForm.for(resource: resource) + # form.subject + # => ['https://ldr.lafayette.edu'] + # form.subject_attributes + # => { '0' => 'https://ldr.lafayette.edu' } + def nested_attributes_for(*fields) + fields.each do |field| + property(:"#{field}_attributes", + virtual: true, + prepopulator: nested_attribute_prepopulator_for(field), + populator: nested_attribute_populator_for(field)) + end + end + + private + + # Maps a Hash of Select2-style Hashes into values for a field. Excludes values where { '_destroy' => 'true' } + def nested_attribute_populator_for(field, value_key: 'id', destroy_key: '_destroy') + lambda do |fragment:, **| + adds = [] + deletes = [] + + fragment.each do |_idx, attrs| + value = attrs[value_key] + if attrs[destroy_key] == 'true' + deletes << value + else + adds << value + end + end + + merged_values = ((Array.wrap(send(field)).map(&:to_s) + adds) - deletes).uniq + send(:"#{field}=", merged_values) + end + end + + # Converts a field into a numbered Hash for use with Select2 inputs. + # + # @example + # form.location + # #=> ['http://sws.geonames.org/5188140/'] + # form.location_attributes + # #=> { '0' => { 'id' => 'http://sws.geonames.org/5188140/'} } + def nested_attribute_prepopulator_for(field, value_key: 'id') + lambda do + attributes = Array.wrap(send(field)) + .each_with_object({}) do |value, attrs| + attrs[attrs.size.to_s] = { value_key => value.to_s } + end + + # @todo do we need an empty value to show a new form line? + attributes[attributes.size.to_s] = { value_key => '' } + + send(:"#{field}_attributes=", attributes) + end + end + end + end + end +end diff --git a/app/indexers/base_resource_indexer.rb b/app/indexers/base_resource_indexer.rb index c54d8feba..dc3afb2fd 100644 --- a/app/indexers/base_resource_indexer.rb +++ b/app/indexers/base_resource_indexer.rb @@ -15,7 +15,7 @@ class BaseResourceIndexer < Hyrax::Indexers::PcdmObjectIndexer class_attribute :years_encompassed_fields, default: [:date] # @todo update the HandleService to generate this and then call from here - # include Spot::IndexesPermalink + # include IndexesPermalink def to_solr super.tap do |doc| @@ -34,7 +34,7 @@ def to_solr private def citation_metadata - return {} unless resource.respond_to?(:bibliographic_citation) + return {} unless resource.respond_to?(:bibliographic_citation) && resource.bibliographic_citation.present? raw = Array.wrap(resource.bibliographic_citation).first parsed = ::AnyStyle.parse(raw)&.first diff --git a/config/initializers/hyrax_events.rb b/config/initializers/hyrax_events.rb index ab83458f9..081bd4e88 100644 --- a/config/initializers/hyrax_events.rb +++ b/config/initializers/hyrax_events.rb @@ -9,9 +9,9 @@ module Spot class ApplicationListener # Mint Handles for records when they are deposited def on_object_deposited(event) - MintHandleJob.perform_later(event[:object]) + # MintHandleJob.perform_later(event[:object]) end end end -Hyrax::Publisher.instance.subscribe(Spot::ApplicationListener.new) +# Hyrax::Publisher.instance.subscribe(Spot::ApplicationListener.new) diff --git a/config/initializers/wings.rb b/config/initializers/wings.rb index 11809025e..72277e633 100644 --- a/config/initializers/wings.rb +++ b/config/initializers/wings.rb @@ -5,26 +5,25 @@ Rails.application.config.after_initialize do # active_fedora models we're migrating [Publication, Image, StudentWork, AudioVisual].each do |work_type| - # Wings::ModelRegistry.register("#{work_type}Resource".constantize, work_type) + # ValkyrieLazyMigration sets up connections between an AF-based work_type and its Valkyrized equivalent + # and also registers the connection in the Wings::ModelRegistry + Hyrax::ValkyrieLazyMigration.migrating("#{work_type}Resource".constantize, from: work_type) # from dassie: # "we register itself so we can pre-translate the class in Freyja instead of having to translate in each query_service" - # Wings::ModelRegistry.register(work_type, work_type) + Wings::ModelRegistry.register(work_type, work_type) - Hyrax::ValkyrieLazyMigration.migrating("#{work_type}Resource".constantize, from: work_type) end # Map AdminSets and Collections - Hyrax::ValkyrieLazyMigration.migrating(AdminSetResource, from: AdminSet) - Hyrax::ValkyrieLazyMigration.migrating(CollectionResource, from: Collection) - - # Wings::ModelRegistry.register(AdminSet, AdminSet) - # Wings::ModelRegistry.register(Collection, Collection) + Hyrax::ValkyrieLazyMigration.migrating(AdminSetResource, from: ::AdminSet) + Hyrax::ValkyrieLazyMigration.migrating(CollectionResource, from: ::Collection) + Wings::ModelRegistry.register(AdminSet, AdminSet) + Wings::ModelRegistry.register(Collection, Collection) + Wings::ModelRegistry.register(FileSet, FileSet) Wings::ModelRegistry.register(Hyrax::FileSet, FileSet) - # Wings::ModelRegistry.register(FileSet, FileSet) - Wings::ModelRegistry.register(Hyrax::FileMetadata, Hydra::PCDM::File) Wings::ModelRegistry.register(Hydra::PCDM::File, Hydra::PCDM::File) @@ -60,21 +59,13 @@ Hyrax::CustomQueries::FindModelsByAccess, Hyrax::CustomQueries::FindCountBy, Hyrax::CustomQueries::FindByDateRange, - # Hyrax::CustomQueries::FindBySourceIdentifier # from bulkrax + Hyrax::CustomQueries::FindBySourceIdentifier # from bulkrax ].each do |handler| Hyrax.query_service.services[0].custom_queries.register_query_handler(handler) end end Rails.application.config.to_prepare do - AdminSetResource.class_eval do - - end - - CollectionResource.class_eval do - attribute :internal_resource, Valkyrie::Types::Any.default('Collection'), internal: true - end - # Copied from Valkyrie.config.resource_class_resolver = lambda do |resource_klass_name| klass_name = resource_klass_name.gsub(/^Wings\((.+)\)$/, '\1') @@ -83,10 +74,6 @@ if resource_types.include?(klass_name) "#{klass_name}Resource".constantize - elsif 'Collection' == klass_name - CollectionResource - elsif 'AdminSet' == klass_name - AdminSetResource # Without this mapping, we'll see cases of Postgres Valkyrie adapter attempting to write to # Fedora. Yeah! elsif 'Hydra::AccessControl' == klass_name From bf0cc5ddceedd8062988bddd2c5c5e6a95c6b8a4 Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Tue, 26 May 2026 14:10:25 -0400 Subject: [PATCH 11/38] publication resource bare-bones submission works --- .gitignore | 3 +++ .../spot/forms/base_resource_form_behavior.rb | 2 -- .../spot/forms/language_tagged_fields.rb | 27 ++++++++++--------- .../concerns/spot/forms/nested_attributes.rb | 7 +++-- app/indexers/base_resource_indexer.rb | 8 ++++++ app/models/user.rb | 11 ++++++++ ...atch_for_pcdm_member_presenters_factory.rb | 27 +++++++++++++++++++ .../derivatives/text_extraction_service.rb | 12 --------- app/services/spot/workflow/activate_object.rb | 15 ++++++----- config/initializers/hyrax.rb | 2 +- config/initializers/spot_overrides.rb | 22 +++++++-------- 11 files changed, 86 insertions(+), 50 deletions(-) create mode 100644 app/presenters/concerns/spot/lucene_patch_for_pcdm_member_presenters_factory.rb diff --git a/.gitignore b/.gitignore index deeb0bd10..1ead26e80 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ /public/uv/* !/public/uv/.keep +# Locally stored files (tbd if this stays?) +/storage + # vendor files /vendor/* !/vendor/.keep diff --git a/app/forms/concerns/spot/forms/base_resource_form_behavior.rb b/app/forms/concerns/spot/forms/base_resource_form_behavior.rb index 6fd37326f..a677dff06 100644 --- a/app/forms/concerns/spot/forms/base_resource_form_behavior.rb +++ b/app/forms/concerns/spot/forms/base_resource_form_behavior.rb @@ -38,8 +38,6 @@ module BaseResourceFormBehavior include Hyrax::FormFields(:core_metadata) include Hyrax::FormFields(:base_metadata) - # form = PublicationResourceForm.for(resource: PublicationResource.new(identifier:['abc:123'])) - if model_class.attribute_names.include?(:identifier) # for local_identifier, we exclude the noid: identifier so as to not let it be user-editable. property :local_identifier, virtual: true, prepopulator: -> { self.local_identifier = model.local_identifier.reject { |id| id.try(:prefix) == 'noid' }.map(&:to_s) } diff --git a/app/forms/concerns/spot/forms/language_tagged_fields.rb b/app/forms/concerns/spot/forms/language_tagged_fields.rb index 540786e8e..ba069a099 100644 --- a/app/forms/concerns/spot/forms/language_tagged_fields.rb +++ b/app/forms/concerns/spot/forms/language_tagged_fields.rb @@ -4,6 +4,18 @@ module Forms module LanguageTaggedFields extend ActiveSupport::Concern + # Helper methods for the language (pre-)populator methods + def rdf_literal_or_value_from(value, language) + return if value.blank? + return value if language.blank? + + RDF::Literal.new(value.to_s, language: language.to_sym) + end + + def deserialize_rdf(value) + (@rdf_serializer ||= RdfLiteralSerializer.new).deserialize(value) + end + module ClassMethods # Provides the option for a field's values to be tagged with a language. # In the form, a field's values are mapped to a _value virtual property @@ -55,7 +67,7 @@ def language_value_prepopulator_for(field) lambda do values = Array.wrap(model.send(field)).map do |value| if value.is_a?(RDF::Literal) - rdf_serializer.deserialize(value)&.value + deserialize_rdf(value)&.value else value.to_s end @@ -70,23 +82,12 @@ def language_value_prepopulator_for(field) def language_prepopulator_for(field) lambda do languages = Array.wrap(model.send(field)).map do |value| - rdf_serializer.deserialize(value)&.language + deserialize_rdf(value)&.language end send(:"#{field}_language=", languages) end end - - def rdf_literal_or_value_from(value, language) - return if value.blank? - return value if language.blank? - - RDF::Literal.new(value.to_s, language: language.to_sym) - end - - def rdf_serializer - @rdf_serializer ||= RdfLiteralSerializer.new - end end end end diff --git a/app/forms/concerns/spot/forms/nested_attributes.rb b/app/forms/concerns/spot/forms/nested_attributes.rb index f6534d64a..f111e069d 100644 --- a/app/forms/concerns/spot/forms/nested_attributes.rb +++ b/app/forms/concerns/spot/forms/nested_attributes.rb @@ -35,7 +35,8 @@ def nested_attribute_populator_for(field, value_key: 'id', destroy_key: '_destro adds = [] deletes = [] - fragment.each do |_idx, attrs| + # :fragment is an ActionController::Parameters object + fragment.to_unsafe_hash.each do |_idx, attrs| value = attrs[value_key] if attrs[destroy_key] == 'true' deletes << value @@ -44,7 +45,9 @@ def nested_attribute_populator_for(field, value_key: 'id', destroy_key: '_destro end end - merged_values = ((Array.wrap(send(field)).map(&:to_s) + adds) - deletes).uniq + original_values = Array.wrap(model.send(field)).map(&:to_s) + merged_values = ((original_values + adds) - deletes).uniq + send(:"#{field}=", merged_values) end end diff --git a/app/indexers/base_resource_indexer.rb b/app/indexers/base_resource_indexer.rb index dc3afb2fd..55c6f87f4 100644 --- a/app/indexers/base_resource_indexer.rb +++ b/app/indexers/base_resource_indexer.rb @@ -28,6 +28,14 @@ def to_solr 'years_encompassed_iim' => parse_years_encompassed } ) + + # Ensure that the solr_document doesn't contain any RDF::Literals, + # as they convert to JSON-LD and Solr throws a fit about JSON-LD keys. + doc.each_pair do |key, val| + if val.is_a?(Array) + doc[key] = val.map { |v| v.is_a?(RDF::Literal) ? v.value : v } + end + end end end diff --git a/app/models/user.rb b/app/models/user.rb index f21e18629..f789c014c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -20,6 +20,17 @@ class User < ApplicationRecord before_save :ensure_username + class << self + # Copied over from Hyrax to overwrite the method in the user concern. + # We remove the password parameter since we don't use it. + # + # @see https://github.com/samvera/hyrax/blob/0af11acf9088cc90c7c9dcf2b4969bd45a101fe2/app/models/concerns/hyrax/user.rb#L183C5-L185C8 + def find_or_create_system_user(user_key) + find_by_user_key(user_key) || create!(user_key_field => user_key) + end + end + + # Does this user belong to the Alumni group? # # @return [true, false] diff --git a/app/presenters/concerns/spot/lucene_patch_for_pcdm_member_presenters_factory.rb b/app/presenters/concerns/spot/lucene_patch_for_pcdm_member_presenters_factory.rb new file mode 100644 index 000000000..33f5c1667 --- /dev/null +++ b/app/presenters/concerns/spot/lucene_patch_for_pcdm_member_presenters_factory.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +module Spot + # iirc we switched the default defType in solr to work better with the + # blacklight advanced-search plugin, or to have better default search + # results? tbh it's lost to time, but Hyrax assumes a lucene default, + # which is causing the pcdm_member_presenters_factory query to fail. + # + # @see https://github.com/samvera/hyrax/blob/hyrax-v5.2.0/app/presenters/hyrax/pcdm_member_presenter_factory.rb#L109-L119 + module LucenePatchForPcdmMemberPresentersFactory + private + + def query_docs(generic_type: nil, ids: object.member_ids) + query = "{!terms f=id}#{ids.join(',')}" + + if generic_type + query += "{!term f=generic_type_si}#{generic_type}" + # works created via ActiveFedora use the _sim field + query += "{!term f=generic_type_sim}#{generic_type}" + end + + Hyrax::SolrService + .post(q: query, rows: 10_000, defType: 'lucene') + .fetch('response') + .fetch('docs') + end + end +end \ No newline at end of file diff --git a/app/services/spot/derivatives/text_extraction_service.rb b/app/services/spot/derivatives/text_extraction_service.rb index 29fe92f91..995ea7721 100644 --- a/app/services/spot/derivatives/text_extraction_service.rb +++ b/app/services/spot/derivatives/text_extraction_service.rb @@ -33,18 +33,6 @@ def create_derivatives(src_path) def valid? pdf_mime_types.include? mime_type end - - # Since the newer Hyrax method is backwards-compatible, let's use that instead of delegating to file_set - # - # @see https://github.com/samvera/hyrax/blob/hyrax-v3.5.0/app/services/hyrax/file_set_derivatives_service.rb#L13-L20 - def uri - # If given a FileMetadata object, use its parent ID. - if file_set.respond_to?(:file_set_id) - file_set.file_set_id.to_s - else - file_set.uri - end - end end end end diff --git a/app/services/spot/workflow/activate_object.rb b/app/services/spot/workflow/activate_object.rb index 8784f9dc9..14ec9abd9 100644 --- a/app/services/spot/workflow/activate_object.rb +++ b/app/services/spot/workflow/activate_object.rb @@ -18,15 +18,16 @@ def self.call(target:, **kwargs) # Since Hyrax::Workflow::ActivateObject is a module (and not a class) # we can't really inherit it, so instead we'll call it Hyrax::Workflow::ActivateObject.call(target: target, **kwargs) + return true if target.respond_to?(:date_available) && target.date_available.present? - if target.respond_to?(:date_available=) && target.date_available.blank? - date = target.embargo_release_date || Time.zone.now - target.date_available = [date.strftime('%Y-%m-%d')] - end + date = if target.try(:embargo) && target.embargo.try(:embargo_release_date).present? + target.embargo.embargo_release_date + else + Time.zone.now + end + + target.date_available = [date.strftime('%Y-%m-%d')] - # Explicitly returning true because the :date_available= guard may return false - # for models without the property defined, which will cause the work to not be saved - # @see https://github.com/samvera/hyrax/blob/v2.9.6/app/services/hyrax/workflow/action_taken_service.rb#L24-L32 true end end diff --git a/config/initializers/hyrax.rb b/config/initializers/hyrax.rb index e92b118be..52483636e 100644 --- a/config/initializers/hyrax.rb +++ b/config/initializers/hyrax.rb @@ -102,7 +102,7 @@ # config.redis_namespace = "hyrax" # Path to the file characterization tool - config.fits_path = ENV.fetch('FITS_PATH') { 'fits.sh' } + config.characterization_options = { ch12n_tool: :fits_servlet } # Path to the file derivatives creation tool config.libreoffice_path = ENV.fetch('SOFFICE_PATH') { 'soffice' } diff --git a/config/initializers/spot_overrides.rb b/config/initializers/spot_overrides.rb index ebf58c6b9..df38167d5 100644 --- a/config/initializers/spot_overrides.rb +++ b/config/initializers/spot_overrides.rb @@ -354,26 +354,29 @@ def authorize_download! Hyrax::DownloadsController.prepend(Spot::HyraxDownloadsControllerDecorator) + # # Encountering an issue where Hyrax::PersistDirectlyContainedOutputFileService.retrieve_file_set requires # Hyrax::UploadedFile#file_set_uri to be an URI but querying for that URI throws an error (ActiveFedora # is appending the base root to the full uri, resulting in errors like: # Ldp::BadRequest: Path contains empty element! /dev/ht/tp/:/http://fedora:8080/rest/dev/2v/23/vt/36/2v23vt362") + # + # @todo This is probably no longer necessary, but keeping it around just in case + # we run into issues converting ActiveFedora objects to ValkyrieResources # module Spot # module HyraxUploadedFileDecorator # def add_file_set!(file_set) # uri = case file_set # when ActiveFedora::Base # file_set.uri - # when Hyrax::Resource + # when Hyrax::Resource, Hyrax::FileSet # file_set.id.is_a?(URI::HTTP) ? file_set.id : Hyrax::Base.id_to_uri(file_set.id.to_s) # end - # update!(file_set_uri: uri) if uri.present? # end # end # end - # Hyrax::UploadedFile.prepend(Spot::HyraxUploadedFileDecorator) + # # Changing the call to open to URI.open because exporters could not find files from URIs otherwise # @@ -422,15 +425,8 @@ def store_files(identifier, folder_count) Bulkrax::CsvParser.prepend(Spot::BulkraxCsvParserDecorator) - # Copied over from Hyrax to overwrite the method in the user concern. - # We remove the password parameter since we don't use it. - # - # @see https://github.com/samvera/hyrax/blob/0af11acf9088cc90c7c9dcf2b4969bd45a101fe2/app/models/concerns/hyrax/user.rb#L183C5-L185C8 - Hyrax::User.class_eval do - def find_or_create_system_user(user_key) - User.find_by_user_key(user_key) || User.create!(user_key_field => user_key) - end - end - Bulkrax::ObjectFactory.prepend(Spot::BulkraxObjectFactoryFindPatch) + + # Patch to use Lucene search to retrieve PCDM Members + Hyrax::PcdmMemberPresenterFactory.prepend(Spot::LucenePatchForPcdmMemberPresentersFactory) end From 820aa5e48b71762b4eda8d68a348ede61b5363c9 Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Tue, 26 May 2026 16:46:10 -0400 Subject: [PATCH 12/38] image derivatives stored in s3 --- Gemfile | 3 + Gemfile.lock | 502 ++++++++++-------- ...atch_for_pcdm_member_presenters_factory.rb | 2 +- .../derivatives/base_derivative_service.rb | 132 ++++- .../derivatives/image_derivative_service.rb | 108 ++++ app/services/spot/s3_path.rb | 39 ++ config/initializers/hyrax.rb | 8 + config/initializers/spot_overrides.rb | 7 - config/initializers/wings.rb | 42 +- 9 files changed, 593 insertions(+), 250 deletions(-) create mode 100644 app/services/spot/derivatives/image_derivative_service.rb create mode 100644 app/services/spot/s3_path.rb diff --git a/Gemfile b/Gemfile index 622f8ce4c..191dd16d2 100644 --- a/Gemfile +++ b/Gemfile @@ -137,6 +137,9 @@ gem 'slack-ruby-client' # used in the Hyrax 4 upgrade but not a dependency?? gem 'twitter-typeahead-rails', '~> 0.11.1' +# use valkyrie-shrine to connect s3 storage to valkyrie +gem 'valkyrie-shrine', '~> 1.1' + # now that we're writing es6 javascript of our own (+ not just using the hyrax js) # we need to compile it in sprockets. # diff --git a/Gemfile.lock b/Gemfile.lock index 7a5a540fe..abebe6435 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -85,7 +85,7 @@ GEM minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.8) + addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) almond-rails (0.3.0) rails (>= 4.2) @@ -96,13 +96,15 @@ GEM wapiti (~> 2.1) anystyle-data (1.3.0) ast (2.4.3) + auth-sanitizer (0.2.1) + version_gem (~> 1.1, >= 1.1.10) autoprefixer-rails (10.4.21.0) execjs (~> 2) - awesome_nested_set (3.8.0) - activerecord (>= 4.0.0, < 8.1) + awesome_nested_set (3.9.0) + activerecord (>= 4.0.0, < 8.2) aws-eventstream (1.4.0) - aws-partitions (1.1172.0) - aws-sdk-core (3.233.0) + aws-partitions (1.1261.0) + aws-sdk-core (3.252.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -110,8 +112,8 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.113.0) - aws-sdk-core (~> 3, >= 3.231.0) + aws-sdk-kms (1.129.0) + aws-sdk-core (~> 3, >= 3.248.0) aws-sigv4 (~> 1.5) aws-sdk-s3 (1.142.0) aws-sdk-core (~> 3, >= 3.189.0) @@ -123,14 +125,12 @@ GEM babel-transpiler (0.7.0) babel-source (>= 4.0, < 6) execjs (~> 2.0) - bagit (0.6.0) + bagit (0.6.1) docopt (~> 0.5.0) validatable (~> 1.6) base64 (0.3.0) - bcp47 (0.3.3) - i18n bcp47_spec (0.2.1) - bcrypt (3.1.20) + bcrypt (3.1.22) bibtex-ruby (6.2.0) latex-decode (~> 0.0) logger (~> 1.7) @@ -142,7 +142,7 @@ GEM rubocop-performance rubocop-rails rubocop-rspec - blacklight (7.41.0) + blacklight (7.42.0) deprecation globalid hashdiff @@ -150,7 +150,7 @@ GEM jbuilder (~> 2.7) kaminari (>= 0.15) ostruct (>= 0.3.2) - rails (>= 6.1, < 8.1) + rails (>= 6.1, < 9) view_component (>= 2.74, < 4) zeitwerk blacklight-access_controls (6.1.0) @@ -171,15 +171,14 @@ GEM blacklight (>= 7.25.2, < 9) deprecation view_component (>= 2.54, < 4) - bootsnap (1.18.6) + bootsnap (1.24.6) msgpack (~> 1.2) - bootstrap (4.6.2) + bootstrap (4.6.2.1) autoprefixer-rails (>= 9.1.0) popper_js (>= 1.16.1, < 2) - sassc-rails (>= 2.0.0) - bootstrap_form (5.1.0) - actionpack (>= 5.2) - activemodel (>= 5.2) + bootstrap_form (5.4.0) + actionpack (>= 6.1) + activemodel (>= 6.1) breadcrumbs_on_rails (3.0.1) browse-everything (1.6.0) addressable (~> 2.5) @@ -192,7 +191,7 @@ GEM ruby-box signet (~> 0.8) builder (3.3.0) - bulkrax (9.3.3) + bulkrax (9.3.5) bagit (~> 0.6.0) coderay denormalize_fields @@ -210,16 +209,16 @@ GEM simple_form byebug (11.1.3) cancancan (3.6.1) - capybara (3.39.2) + capybara (3.40.0) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - capybara-screenshot (1.0.26) + capybara-screenshot (1.0.27) capybara (>= 1.0, < 4) launchy carrierwave (1.3.4) @@ -239,8 +238,9 @@ GEM execjs coffee-script-source (1.12.2) concurrent-ruby (1.3.4) - connection_pool (2.5.4) - crack (1.0.0) + connection_pool (2.5.5) + content_disposition (1.0.0) + crack (1.0.1) bigdecimal rexml crass (1.0.6) @@ -250,7 +250,7 @@ GEM database_cleaner-active_record (2.2.2) activerecord (>= 5.a) database_cleaner-core (~> 2.0) - database_cleaner-core (2.0.1) + database_cleaner-core (2.1.0) date (3.5.1) declarative (0.0.20) denormalize_fields (1.3.0) @@ -278,13 +278,17 @@ GEM dotenv-rails (2.7.6) dotenv (= 2.7.6) railties (>= 3.2) - draper (4.0.4) + down (5.6.0) + addressable (~> 2.8) + base64 (~> 0.3) + draper (4.0.6) actionpack (>= 5.0) activemodel (>= 5.0) activemodel-serializers-xml (>= 1.0) activesupport (>= 5.0) request_store (>= 1.0) ruby2_keywords + drb (2.2.3) dropbox_api (0.1.21) faraday (< 3.0) oauth2 (~> 1.1) @@ -293,39 +297,39 @@ GEM zeitwerk (~> 2.6) dry-container (0.11.0) concurrent-ruby (~> 1.0) - dry-core (1.1.0) + dry-core (1.2.0) concurrent-ruby (~> 1.0) logger zeitwerk (~> 2.6) dry-events (1.1.0) concurrent-ruby (~> 1.0) dry-core (~> 1.1) - dry-inflector (1.2.0) + dry-inflector (1.3.1) dry-initializer (3.2.0) dry-logic (1.6.0) bigdecimal concurrent-ruby (~> 1.0) dry-core (~> 1.1) zeitwerk (~> 2.6) - dry-monads (1.9.0) + dry-monads (1.10.0) concurrent-ruby (~> 1.0) dry-core (~> 1.1) zeitwerk (~> 2.6) - dry-schema (1.14.1) + dry-schema (1.16.0) concurrent-ruby (~> 1.0) dry-configurable (~> 1.0, >= 1.0.1) dry-core (~> 1.1) dry-initializer (~> 3.2) - dry-logic (~> 1.5) - dry-types (~> 1.8) + dry-logic (~> 1.6) + dry-types (~> 1.9, >= 1.9.1) zeitwerk (~> 2.6) - dry-struct (1.8.0) + dry-struct (1.8.1) dry-core (~> 1.1) dry-types (~> 1.8, >= 1.8.2) ice_nine (~> 0.11) zeitwerk (~> 2.6) - dry-types (1.8.3) - bigdecimal (~> 3.0) + dry-types (1.9.1) + bigdecimal (>= 3.0) concurrent-ruby (~> 1.0) dry-core (~> 1.0) dry-inflector (~> 1.0) @@ -337,11 +341,12 @@ GEM dry-initializer (~> 3.2) dry-schema (~> 1.14) zeitwerk (~> 2.6) - ebnf (2.4.0) + ebnf (2.6.0) + base64 (~> 0.2) htmlentities (~> 4.3) rdf (~> 3.3) scanf (~> 1.0) - sxp (~> 1.3) + sxp (~> 2.0) unicode-types (~> 1.8) edtf (3.1.1) activesupport (>= 3.0, < 8.0) @@ -354,32 +359,31 @@ GEM erubi (1.13.1) et-orbi (1.4.0) tzinfo - execjs (2.10.0) - factory_bot (6.4.5) - activesupport (>= 5.0.0) - factory_bot_rails (6.4.3) - factory_bot (~> 6.4) - railties (>= 5.0.0) - faraday (2.14.0) + execjs (2.10.1) + factory_bot (6.6.0) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + faraday (2.14.3) faraday-net_http (>= 2.0, < 3.5) json logger faraday-encoding (0.0.6) faraday - faraday-follow_redirects (0.4.0) + faraday-follow_redirects (0.5.0) faraday (>= 1, < 3) - faraday-mashify (1.0.0) + faraday-mashify (1.0.2) faraday (~> 2.0) hashie - faraday-multipart (1.1.1) + faraday-multipart (1.2.0) multipart-post (~> 2.0) - faraday-net_http (3.4.2) + faraday-net_http (3.4.4) net-http (~> 0.5) - faraday-retry (2.3.2) + faraday-retry (2.4.0) faraday (~> 2.0) - ffi (1.17.2) - ffprober (1.0) - sorbet-runtime + ffi (1.17.4-arm64-darwin) + ffprober (2.0) flipflop (2.8.0) activesupport (>= 4.0) terminal-table (>= 1.8) @@ -387,10 +391,10 @@ GEM jquery-rails font-awesome-rails (4.7.0.9) railties (>= 3.2, < 9.0) - fugit (1.12.1) + fugit (1.12.2) et-orbi (~> 1.4) raabro (~> 1.4) - gapic-common (1.2.0) + gapic-common (1.3.0) faraday (>= 1.9, < 3.a) faraday-retry (>= 1.0, < 3.a) google-cloud-env (~> 2.2) @@ -407,69 +411,71 @@ GEM ostruct globalid (1.3.0) activesupport (>= 6.1) - google-analytics-data (0.7.2) + google-analytics-data (0.9.0) google-analytics-data-v1beta (>= 0.11, < 2.a) google-cloud-core (~> 1.6) - google-analytics-data-v1beta (0.19.0) - gapic-common (~> 1.2) + google-analytics-data-v1beta (0.22.0) + gapic-common (~> 1.3) google-cloud-errors (~> 1.0) - google-apis-core (1.0.2) - addressable (~> 2.8, >= 2.8.7) + google-apis-core (1.2.3) + addressable (~> 2.9) faraday (~> 2.13) faraday-follow_redirects (~> 0.3) googleauth (~> 1.14) mini_mime (~> 1.1) + multi_json (~> 1.11) representable (~> 3.0) - retriable (~> 3.1) - google-apis-drive_v3 (0.75.0) + retriable (>= 3.1, < 5.0) + google-apis-drive_v3 (0.81.0) google-apis-core (>= 0.15.0, < 2.a) - google-cloud-core (1.8.0) + google-cloud-core (1.9.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (2.3.1) base64 (~> 0.2) faraday (>= 1.0, < 3.a) - google-cloud-errors (1.5.0) + google-cloud-errors (1.6.0) google-logging-utils (0.2.0) - google-protobuf (4.33.0) + google-protobuf (4.35.1-arm64-darwin) bigdecimal - rake (>= 13) - googleapis-common-protos (1.9.0) + rake (~> 13.3) + googleapis-common-protos (1.10.0) google-protobuf (~> 4.26) googleapis-common-protos-types (~> 1.21) grpc (~> 1.41) - googleapis-common-protos-types (1.22.0) + googleapis-common-protos-types (1.23.0) google-protobuf (~> 4.26) - googleauth (1.15.1) + googleauth (1.17.1) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.2) google-logging-utils (~> 0.1) jwt (>= 1.4, < 4.0) - multi_json (~> 1.11) os (>= 0.9, < 2.0) + pstore (~> 0.1) signet (>= 0.16, < 2.a) - grpc (1.75.0) + grpc (1.81.1-arm64-darwin) google-protobuf (>= 3.25, < 5.0) googleapis-common-protos-types (~> 1.0) - haml (6.3.0) + haml (6.4.0) temple (>= 0.8.2) thor tilt hashdiff (1.2.1) - hashie (5.0.0) + hashie (5.1.0) + logger hiredis (0.6.3) honeybadger (4.12.2) htmlentities (4.4.2) - http_logger (1.0.1) - hydra-access-controls (13.1.0) + http_logger (1.0.2) + hydra-access-controls (13.2.0) active-fedora (>= 10.0.0) - activesupport (>= 6.1, < 8.1) + activesupport (>= 6.1, < 9) blacklight-access_controls (~> 6.0) cancancan (>= 1.8, < 4) deprecation (~> 1.0) - hydra-core (13.1.0) - hydra-access-controls (= 13.1.0) - railties (>= 6.1, < 8.1) + hydra-core (13.2.0) + hydra-access-controls (= 13.2.0) + railties (>= 6.1, < 9) hydra-derivatives (4.1.0) active-fedora (>= 14.0) active-triples (>= 1.2) @@ -480,23 +486,23 @@ GEM mime-types (> 2.0, < 4.0) mini_magick (>= 3.2, < 5) ruby-vips - hydra-editor (7.0.0) + hydra-editor (7.1.0) active-fedora (>= 9.0.0) - activerecord (>= 5.2, < 8.0) + activerecord (>= 5.2, < 8.1) almond-rails (~> 0.1) cancancan concurrent-ruby (= 1.3.4) psych (~> 3.3, < 4) - rails (>= 5.2, < 8.0) + rails (>= 5.2, < 8.1) simple_form (>= 4.1.0, < 5.2) - sprockets (>= 3.7) + sprockets (~> 3.7) sprockets-es6 hydra-file_characterization (1.2.0) activesupport (>= 3.0.0) - hydra-head (13.1.0) - hydra-access-controls (= 13.1.0) - hydra-core (= 13.1.0) - rails (>= 6.1, < 8.1) + hydra-head (13.2.0) + hydra-access-controls (= 13.2.0) + hydra-core (= 13.2.0) + rails (>= 6.1, < 9) hydra-pcdm (1.4.0) active-fedora (>= 10) mime-types (>= 1) @@ -508,8 +514,8 @@ GEM cancancan json (>= 1.8) psych (~> 3.0) - hydra-works (2.2.0) - activesupport (>= 5.2, < 8.0) + hydra-works (2.3.0) + activesupport (>= 5.2, < 9.0) hydra-derivatives (>= 3.6) hydra-file_characterization (~> 1.0) hydra-pcdm (>= 0.9) @@ -588,7 +594,7 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.18.0) + json (2.19.9) json-canonicalization (1.0.0) json-ld (3.3.2) htmlentities (~> 4.3) @@ -598,13 +604,13 @@ GEM rack (>= 2.2, < 4) rdf (~> 3.3) rexml (~> 3.2) - json-ld-preloaded (3.2.2) - json-ld (~> 3.2) - rdf (~> 3.2) - json-schema (6.0.0) + json-ld-preloaded (3.3.2) + json-ld (~> 3.3) + rdf (~> 3.3) + json-schema (6.2.0) addressable (~> 2.8) - bigdecimal (~> 3.1) - jwt (2.10.2) + bigdecimal (>= 3.1, < 5) + jwt (2.10.3) base64 kaminari (1.2.2) activesupport (>= 4.1.0) @@ -619,25 +625,25 @@ GEM kaminari-core (= 1.2.2) kaminari-core (1.2.2) language_list (1.2.1) - latex-decode (0.4.0) + latex-decode (0.4.2) launchy (3.1.1) addressable (~> 2.8) childprocess (~> 5.0) logger (~> 1.6) - ld-patch (3.2.2) - ebnf (~> 2.3) - rdf (~> 3.2) - rdf-xsd (~> 3.2) - sparql (~> 3.2) - sxp (~> 1.2) - ldp (1.2.0) + ld-patch (3.3.1) + ebnf (~> 2.6) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + sparql (~> 3.3) + sxp (~> 2.0) + ldp (1.2.1) deprecation faraday (>= 1) http_logger json-ld (~> 3.2) rdf (~> 3.2) rdf-isomorphic - rdf-ldp + rdf-ldp (>= 2.1) rdf-turtle rdf-vocab (>= 0.8) slop @@ -648,7 +654,7 @@ GEM rdf-vocab (~> 3.0) legato (0.7.0) multi_json - libxml-ruby (5.0.5) + libxml-ruby (5.0.6) link_header (0.0.8) linkeddata (3.1.6) equivalent-xml (~> 0.6) @@ -686,7 +692,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.25.0) + loofah (2.25.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.9.0) @@ -698,23 +704,25 @@ GEM mailboxer (0.15.1) carrierwave (>= 0.5.8) rails (>= 5.0.0) - marcel (1.1.0) + marcel (1.2.1) matrix (0.4.3) method_source (1.1.0) mime-types (3.7.0) logger mime-types-data (~> 3.2025, >= 3.2025.0507) - mime-types-data (3.2026.0113) + mime-types-data (3.2026.0414) mini_magick (4.13.2) mini_mime (1.1.5) - mini_portile2 (2.8.9) - minitest (6.0.1) + minitest (6.0.6) + drb (~> 2.0) prism (~> 1.5) - msgpack (1.8.0) + msgpack (1.8.3) multi_json (1.20.1) - multi_xml (0.7.2) - bigdecimal (~> 3.1) + multi_xml (0.9.1) + bigdecimal (>= 3.1, < 5) multipart-post (2.4.1) + mustermann (2.0.2) + ruby2_keywords (~> 0.0.1) mutex_m (0.3.0) namae (1.2.0) racc (~> 1.7) @@ -722,9 +730,9 @@ GEM redic net-http (0.9.1) uri (>= 0.11.1) - net-http-persistent (4.0.6) - connection_pool (~> 2.2, >= 2.2.4) - net-imap (0.6.2) + net-http-persistent (4.0.8) + connection_pool (>= 2.2.4, < 4) + net-imap (0.6.4.1) date net-protocol net-pop (0.1.2) @@ -738,8 +746,7 @@ GEM noid-rails (3.3.0) actionpack (>= 5.0.0, < 9) noid (~> 0.9) - nokogiri (1.19.0) - mini_portile2 (~> 2.8.2) + nokogiri (1.19.3-arm64-darwin) racc (~> 1.4) non-digest-assets (2.2.0) activesupport (>= 5.2, < 7.1) @@ -749,12 +756,15 @@ GEM faraday (< 3) faraday-follow_redirects (>= 0.3.0, < 2) rexml - oauth (1.1.2) - oauth-tty (~> 1.0, >= 1.0.6) - snaky_hash (~> 2.0) - version_gem (~> 1.1, >= 1.1.9) - oauth-tty (1.0.6) - version_gem (~> 1.1, >= 1.1.9) + oauth (1.1.7) + auth-sanitizer (~> 0.2, >= 0.2.1) + base64 (~> 0.1) + oauth-tty (~> 1.0, >= 1.0.10) + snaky_hash (~> 2.0, >= 2.0.5) + version_gem (~> 1.1, >= 1.1.12) + oauth-tty (1.0.10) + auth-sanitizer (~> 0.2, >= 0.2.1) + version_gem (~> 1.1, >= 1.1.12) oauth2 (1.4.11) faraday (>= 0.17.3, < 3.0) jwt (>= 1.0, < 3.0) @@ -767,38 +777,45 @@ GEM orm_adapter (0.5.0) os (1.1.4) ostruct (0.6.3) - parallel (1.27.0) - parser (3.3.9.0) + parallel (1.28.0) + parser (3.3.11.1) ast (~> 2.4.1) racc parslet (2.0.0) pg (1.5.9) popper_js (1.16.1) posix-spawn (0.3.15) - prism (1.5.2) + prism (1.9.0) + pstore (0.2.1) psych (3.3.4) - public_suffix (7.0.2) + public_suffix (7.0.5) puma (6.4.3) nio4r (~> 2.0) - qa (5.15.0) + qa (5.16.0) activerecord-import deprecation faraday (< 3.0, != 2.0.0) geocoder ldpath nokogiri (~> 1.6) - rails (>= 5.0, < 8.1) + rails (>= 6.0, < 8.2) rdf raabro (1.4.0) racc (1.8.1) - rack (2.2.21) + rack (2.2.23) rack-cas (0.16.1) addressable (~> 2.3) nokogiri (~> 1.5) rack (>= 1.3) - rack-protection (3.2.0) - base64 (>= 0.1.0) - rack (~> 2.2, >= 2.2.4) + rack-linkeddata (3.1.3) + linkeddata (~> 3.1, >= 3.1.6) + rack (~> 2.1) + rack-rdf (~> 3.1, >= 3.1.2) + rack-protection (2.2.4) + rack + rack-rdf (3.3.0) + rack (>= 2.2, < 4) + rdf (~> 3.3) rack-test (2.2.0) rack (>= 1.3) rails (6.1.7.10) @@ -824,8 +841,8 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.2) - loofah (~> 2.21) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails_autolink (1.1.8) actionview (> 3.1) @@ -838,7 +855,7 @@ GEM rake (>= 12.2) thor (~> 1.0) rainbow (3.1.1) - rake (13.3.1) + rake (13.4.2) rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) @@ -849,66 +866,75 @@ GEM logger (~> 1.5) ostruct (~> 0.6) readline (~> 0.0) - rdf-aggregate-repo (3.2.1) - rdf (~> 3.2) + rdf-aggregate-repo (3.3.0) + rdf (~> 3.3) rdf-isomorphic (3.3.0) rdf (~> 3.3) - rdf-json (3.2.0) + rdf-json (3.3.0) + rdf (~> 3.3) + rdf-ldp (2.1.0) + json-ld (~> 3.2) + ld-patch (~> 3.2) + link_header (~> 0.0, >= 0.0.8) + rack (~> 2.2) + rack-linkeddata (~> 3.1) rdf (~> 3.2) - rdf-ldp (0.1.0) - deprecation - rdf - rdf-microdata (3.2.1) + rdf-turtle (~> 3.2) + rdf-vocab (~> 3.2) + sinatra (~> 2.1) + rdf-microdata (3.3.0) htmlentities (~> 4.3) - nokogiri (~> 1.13) - rdf (~> 3.2) - rdf-rdfa (~> 3.2) - rdf-xsd (~> 3.2) - rdf-n3 (3.2.1) - ebnf (~> 2.2) - rdf (~> 3.2) - sparql (~> 3.2) - sxp (~> 1.2) - rdf-normalize (0.6.1) - rdf (~> 3.2) - rdf-ordered-repo (3.2.1) - rdf (~> 3.2, >= 3.2.1) - rdf-rdfa (3.2.3) - haml (>= 5.2, < 7) + nokogiri (~> 1.15, >= 1.15.4) + rdf (~> 3.3) + rdf-rdfa (~> 3.3) + rdf-xsd (~> 3.3) + rdf-n3 (3.3.1) + ebnf (~> 2.5) + rdf (~> 3.3) + sparql (~> 3.3) + sxp (~> 2.0) + rdf-normalize (0.7.0) + rdf (~> 3.3) + rdf-ordered-repo (3.3.0) + rdf (~> 3.3) + rdf-rdfa (3.3.0) + haml (~> 6.1) htmlentities (~> 4.3) - rdf (~> 3.2) - rdf-aggregate-repo (~> 3.2) - rdf-vocab (~> 3.2) - rdf-xsd (~> 3.2) - rdf-rdfxml (3.2.2) - builder (~> 3.2) + rdf (~> 3.3) + rdf-aggregate-repo (~> 3.3) + rdf-vocab (~> 3.3) + rdf-xsd (~> 3.3) + rdf-rdfxml (3.3.0) + builder (~> 3.2, >= 3.2.4) htmlentities (~> 4.3) - rdf (~> 3.2) - rdf-xsd (~> 3.2) - rdf-reasoner (0.8.0) - rdf (~> 3.2) - rdf-xsd (~> 3.2) - rdf-tabular (3.2.1) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + rdf-reasoner (0.9.0) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + rdf-tabular (3.3.0) addressable (~> 2.8) - bcp47 (~> 0.3, >= 0.3.3) - json-ld (~> 3.2) - rdf (~> 3.2, >= 3.2.7) - rdf-vocab (~> 3.2) - rdf-xsd (~> 3.2) + bcp47_spec (~> 0.2) + json-ld (~> 3.3) + rdf (~> 3.3) + rdf-vocab (~> 3.3) + rdf-xsd (~> 3.3) rdf-trig (3.3.0) ebnf (~> 2.4) rdf (~> 3.3) rdf-turtle (~> 3.3) - rdf-trix (3.2.0) - rdf (~> 3.2) - rdf-xsd (~> 3.2) - rdf-turtle (3.3.0) - ebnf (~> 2.4) + rdf-trix (3.3.0) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + rdf-turtle (3.3.1) + base64 (~> 0.2) + bigdecimal (~> 3.1, >= 3.1.5) + ebnf (~> 2.5) rdf (~> 3.3) rdf-vocab (3.3.3) rdf (~> 3.3) - rdf-xsd (3.2.1) - rdf (~> 3.2) + rdf-xsd (3.3.0) + rdf (~> 3.3) rexml (~> 3.2) readline (0.0.4) reline @@ -926,7 +952,7 @@ GEM reform-rails (0.2.6) activemodel (>= 5.0) reform (>= 2.3.1, < 3.0.0) - regexp_parser (2.11.3) + regexp_parser (2.12.0) reline (0.6.3) io-console (~> 0.5) representable (3.2.0) @@ -938,13 +964,13 @@ GEM responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) - retriable (3.1.2) + retriable (3.8.0) rexml (3.4.4) roman (0.2.0) rsolr (2.5.0) builder (>= 2.1.2) faraday (>= 0.9, < 3, != 2.0.0) - rspec (3.13.1) + rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) @@ -956,7 +982,7 @@ GEM rspec-its (1.3.1) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.13.6) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-rails (5.1.2) @@ -967,7 +993,7 @@ GEM rspec-expectations (~> 3.10) rspec-mocks (~> 3.10) rspec-support (~> 3.10) - rspec-support (3.13.6) + rspec-support (3.13.7) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) rubocop (1.28.2) @@ -979,9 +1005,9 @@ GEM rubocop-ast (>= 1.17.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.47.1) + rubocop-ast (1.49.1) parser (>= 3.3.7.2) - prism (~> 1.4) + prism (~> 1.7) rubocop-performance (1.19.1) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) @@ -1014,26 +1040,31 @@ GEM tilt scanf (1.0.0) select2-rails (3.5.11) - selenium-webdriver (4.9.0) + selenium-webdriver (4.44.0) + base64 (~> 0.2) + logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) + rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) - shacl (0.3.0) - json-ld (~> 3.2) - rdf (~> 3.2, >= 3.2.8) - sparql (~> 3.2, >= 3.2.4) - sxp (~> 1.2) - shex (0.7.1) - ebnf (~> 2.2) + shacl (0.4.3) + json-ld (~> 3.3) + rdf (~> 3.3) + sparql (~> 3.3) + sxp (~> 2.0) + shex (0.8.1) + ebnf (~> 2.5) htmlentities (~> 4.3) - json-ld (~> 3.2) - json-ld-preloaded (~> 3.2) - rdf (~> 3.2) - rdf-xsd (~> 3.2) - sparql (~> 3.2) - sxp (~> 1.2) + json-ld (~> 3.3) + json-ld-preloaded (~> 3.3) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + sparql (~> 3.3) + sxp (~> 2.0) shoulda-matchers (4.5.1) activesupport (>= 4.2.0) + shrine (3.7.1) + content_disposition (~> 1.0) + down (~> 5.1) sidekiq (5.2.10) connection_pool (~> 2.2, >= 2.2.2) rack (~> 2.0) @@ -1042,11 +1073,10 @@ GEM sidekiq-cron (1.9.1) fugit (~> 1.8) sidekiq (>= 4.2.1) - signet (0.21.0) + signet (0.22.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 4.0) - multi_json (~> 1.10) simple_form (5.1.0) actionpack (>= 5.2) activemodel (>= 5.2) @@ -1054,12 +1084,17 @@ GEM docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-cobertura (3.1.0) + simplecov-cobertura (3.2.0) rexml simplecov (~> 0.19) simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) - slack-ruby-client (3.0.0) + sinatra (2.2.4) + mustermann (~> 2.0) + rack (~> 2.2) + rack-protection (= 2.2.4) + tilt (~> 2.0) + slack-ruby-client (3.1.0) faraday (>= 2.0.1) faraday-mashify faraday-multipart @@ -1067,22 +1102,22 @@ GEM hashie logger slop (4.10.1) - snaky_hash (2.0.3) + snaky_hash (2.0.6) hashie (>= 0.1.0, < 6) version_gem (>= 1.1.8, < 3) - sorbet-runtime (0.5.12443) - sparql (3.2.6) + sparql (3.3.2) builder (~> 3.2, >= 3.2.4) - ebnf (~> 2.3, >= 2.3.5) + ebnf (~> 2.5) logger (~> 1.5) - rdf (~> 3.2, >= 3.2.11) - rdf-aggregate-repo (~> 3.2, >= 3.2.1) - rdf-xsd (~> 3.2) - sparql-client (~> 3.2, >= 3.2.2) - sxp (~> 1.2, >= 1.2.4) - sparql-client (3.2.2) + rdf (~> 3.3) + rdf-aggregate-repo (~> 3.3) + rdf-xsd (~> 3.3) + readline (~> 0.0) + sparql-client (~> 3.3) + sxp (~> 2.0) + sparql-client (3.3.0) net-http-persistent (~> 4.0, >= 4.0.2) - rdf (~> 3.2, >= 3.2.11) + rdf (~> 3.3) spring (2.1.1) spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) @@ -1101,7 +1136,7 @@ GEM ssrf_filter (1.0.8) stub_env (1.0.4) rspec (>= 2.0, < 4.0) - sxp (1.3.0) + sxp (2.0.0) matrix (~> 0.4) rdf (~> 3.3) temple (0.10.4) @@ -1110,8 +1145,8 @@ GEM terser (1.2.7) execjs (>= 0.3.0, < 3) thor (1.5.0) - tilt (2.6.1) - timeout (0.6.0) + tilt (2.7.0) + timeout (0.6.1) tinymce-rails (5.10.9) railties (>= 3.1.1) trailblazer-option (0.1.2) @@ -1129,7 +1164,7 @@ GEM unicode-types (1.11.0) uri (1.1.1) validatable (1.6.7) - valkyrie (3.5.0) + valkyrie (3.5.1) activemodel activesupport dry-struct @@ -1139,11 +1174,15 @@ GEM json json-ld railties - rdf (~> 3.0, >= 3.0.10) + rdf (~> 3.0, >= 3.3.2) rdf-vocab reform (~> 2.2) reform-rails - version_gem (1.1.9) + valkyrie-shrine (1.1.0) + aws-sdk-s3 (~> 1) + shrine (>= 2.0, < 4.0) + valkyrie (> 1.0) + version_gem (1.1.12) view_component (2.74.1) activesupport (>= 5.0.0, < 8.0) concurrent-ruby (~> 1.0) @@ -1153,21 +1192,21 @@ GEM rexml (~> 3.0) warden (1.2.9) rack (>= 2.0.9) - webmock (3.25.1) + webmock (3.26.2) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) websocket (1.2.11) - websocket-driver (0.8.0) + websocket-driver (0.8.1) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.4) + zeitwerk (2.8.2) PLATFORMS - ruby + arm64-darwin-24 DEPENDENCIES almond-rails (~> 0.3.0) @@ -1241,6 +1280,7 @@ DEPENDENCIES terser (~> 1.2.7) turbolinks (~> 5.2.1) twitter-typeahead-rails (~> 0.11.1) + valkyrie-shrine (~> 1.1) webmock (~> 3.8) BUNDLED WITH diff --git a/app/presenters/concerns/spot/lucene_patch_for_pcdm_member_presenters_factory.rb b/app/presenters/concerns/spot/lucene_patch_for_pcdm_member_presenters_factory.rb index 33f5c1667..34a59bbd6 100644 --- a/app/presenters/concerns/spot/lucene_patch_for_pcdm_member_presenters_factory.rb +++ b/app/presenters/concerns/spot/lucene_patch_for_pcdm_member_presenters_factory.rb @@ -24,4 +24,4 @@ def query_docs(generic_type: nil, ids: object.member_ids) .fetch('docs') end end -end \ No newline at end of file +end diff --git a/app/services/spot/derivatives/base_derivative_service.rb b/app/services/spot/derivatives/base_derivative_service.rb index 0494520a9..60f9dc9c8 100644 --- a/app/services/spot/derivatives/base_derivative_service.rb +++ b/app/services/spot/derivatives/base_derivative_service.rb @@ -1,14 +1,130 @@ # frozen_string_literal: true module Spot module Derivatives - # Base class that other derivative services can inherit from - class BaseDerivativeService < ::Hyrax::DerivativeService - delegate :audio_mime_types, - :image_mime_types, - :pdf_mime_types, - :office_document_mime_types, - :video_mime_types, - to: :FileSet + # + # + # Hyrax derivative service options are set in Hyrax.config.derivative_services and + # determined by the first to return true to #valid? Hyrax provides a catch-all + # Hyrax::FileSetDerivativesService that I recommend including at the end of the + # custom derivative services to pick up file_types not covered by the others. + # + # @example configuring services + # # config/initializers/hyrax.rb + # Hyrax.configure do |config| + # config.derivative_services = [ + # Spot::ImageDerivativesService, + # Spot::BaseDerivativeService, + # Hyrax::FileSetDerivativesService + # ] + # end + # + # @example Subclassing to handle edge cases + # class CoolCustomDerivativeService < Spot::Derivatives::BaseDerivativeService + # def cleanup_derivatives + # super # delete thumbnail if exists + # # idk cleanup + # end + # + # def create_derivatives(file_name) + # super # generate thumbnails + text extract (where applicable) + # do_something_with_this_type(file_name) + # end + # + # def valid? + # file_set.label.include?('transcript') + # end + # + # private + # + # def do_something_with_this_type(file_name) + # # ... + # end + # end + # + # Hyrax.config.derivative_services = [ + # CoolCustomDerivativeService, + # Spot::Derivatives::BaseDerivativeService + # Hyrax::FileSetDerivativesService + # ] + # + # @see https://github.com/samvera/hyrax/blob/hyrax-v4.0.0/app/jobs/valkyrie_create_derivatives_job.rb#L10 + # @see https://github.com/samvera/hyrax/blob/hyrax-v4.0.0/app/services/hyrax/file_set_derivatives_service.rb + class BaseDerivativeService + delegate :audio_mime_types, :image_mime_types, :pdf_mime_types, :office_document_mime_types, :video_mime_types, to: :FileSet + delegate :mime_type, to: :file_set + + attr_reader :file_set + + def initialize(file_set) + @file_set = file_set + end + + def cleanup_derivatives + delete_thumbnail! + end + + def create_derivatives(src_path) + create_thumbnail_from(src_path) + extract_and_save_full_text(src_path) if full_text_eligible_types.include?(mime_type) + end + + def valid? + [*pdf_mime_types, *office_document_mime_types].include?(mime_type) + end + + private + + def create_thumbnail_from(path) + MiniMagick::Tool::Convert.new do |convert| + convert.merge!( + [ + "#{path}[0]", + "-colorspace", "sRGB", + "-flatten", + "-resize", "200x150>", + "-format", "jpg", + thumbnail_derivative_path + ] + ) + end + end + + # Copied from Hyrax::FileSetDerivativeService + # + # @see https://github.com/samvera/hyrax/blob/hyrax-v4.0.0/app/services/hyrax/file_set_derivatives_service.rb#L119-L127 + def extract_and_save_full_text(src_path) + return unless Hyrax.config.extract_full_text? + + Rails.logger.warn 'Skipping full-text extraction for the moment' + # outputs = [{ url: full_text_target_uri, container: 'extracted_text' }] + # Hydra::Derivatives::FullTextExtract.create(src_path, outputs: outputs) + end + + def delete_thumbnail! + FileUtils.rm_f(thumbnail_derivative_path) if File.exist?(thumbnail_derivative_path) + end + + def full_text_eligible_types + [*image_mime_types, *pdf_mime_types, *office_document_mime_types] + end + + # @see https://github.com/samvera/hyrax/blob/hyrax-v3.5.0/app/services/hyrax/file_set_derivatives_service.rb#L13-L20 + def full_text_target_uri + # If given a FileMetadata object, use its parent ID. + if file_set.respond_to?(:file_set_id) + file_set.file_set_id.to_s + else + file_set.uri + end + end + + def thumbnail_derivative_path + return @thumbnail_derivative_path if @thumbnail_derivative_path.present? + + @thumbnail_derivative_path = Hyrax::DerivativePath.derivative_path_for_reference(file_set, 'thumbnail').to_s.tap do |path| + FileUtils.mkdir_p(File.dirname(path)) unless Dir.exist?(File.dirname(path)) + end + end end end end diff --git a/app/services/spot/derivatives/image_derivative_service.rb b/app/services/spot/derivatives/image_derivative_service.rb new file mode 100644 index 000000000..8ab526d16 --- /dev/null +++ b/app/services/spot/derivatives/image_derivative_service.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true +module Spot + module Derivatives + # Creates pyramidal TIFF copies of Images for serving via IIIF. Pyramidal TIFFs contain + # layers at different resolutions which makes their use in a deep-zooming IIIF application + # (ie. UniversalViewer) more efficient. + # + # This generates the file locally and then uploads to an S3 bucket defined by the + # AWS_IIIF_ASSET_BUCKET environment variable. The local copy is deleted afterwards. + # + # These derivatives are created for an FileSets that include Image mime_types. + # + # @see https://www.loc.gov/preservation/digital/formats/fdd/fdd000237.shtml + class ImageDerivativeService < BaseDerivativeService + # Deletes the derivative from the S3 bucket using the Valkyrie storage adapter + # @todo maybe we should hang onto these when we delete + put them in a glacier grave? + # @return [void] + def cleanup_derivatives + super + + storage_adapter.delete(id: File.basename(shuttle_file)) + end + + # Generates a pyramidal TIFF using ImageMagick (via MiniMagick gem) + # and uploads it to the S3 bucket via Valkyrie StorageAdapter. + # + # @param [String,Pathname] filename the src path of the file + # @return [void] + # @todo do we delete the working copy or just let it hang in tmp/uploads? + def create_derivatives(filename) + super + + create_and_upload_iiif_access_copy(filename) + end + + # Only create pyramidal TIFFs if the source mime_type is an Image and if we defined + def valid? + return no_bucket_warning if s3_bucket.blank? + + image_mime_types.include?(mime_type) + end + + private + + # Create a pyramidal tiff derivative from the pathname provided + # and upload it to our IIIF S3 bucket with the name `-access.tif`. + # The intermediary file is deleted after upload. + def create_and_upload_iiif_access_copy(filename) + return no_bucket_warning if s3_bucket.blank? + + create_access_copy_from(filename) + upload_derivatives_to_s3 && delete_shuttle_file! + end + + def create_access_copy_from(src) + MiniMagick::Tool::Convert.new do |convert| + convert.merge!( + [ + "#{src}[0]", + "-define", "tiff:tile-geometry=128x128", + "-compress", "jpeg", + "ptif:#{shuttle_file}" + ] + ) + end + end + + def delete_shuttle_file! + FileUtils.rm_f(shuttle_file) if File.exist?(shuttle_file) + end + + def no_bucket_warning + Rails.logger.warn('Skipping IIIF Access Copy generation because the AWS_IIIF_ASSET_BUCKET environment variable is not defined.') + false + end + + def s3_bucket + ENV['AWS_IIIF_ASSET_BUCKET'] + end + + def shuttle_file + working_directory.join("#{file_set.id}-access.tif") + end + + def storage_adapter + Valkyrie::StorageAdapter.find(:iiif_source_s3) + end + + def upload_derivatives_to_s3 + storage_adapter.upload( + resource: file_set, + file: File.open(shuttle_file), + original_filename: File.basename(shuttle_file), + metadata: { + 'width' => file_set.width.first, + 'height' => file_set.height.first + } + ) + end + + def working_directory + @working_directory ||= Rails.root.join('tmp', 'iiif-src').tap do |src| + FileUtils.mkdir_p(src) unless Dir.exist?(src) + end + end + end + end +end \ No newline at end of file diff --git a/app/services/spot/s3_path.rb b/app/services/spot/s3_path.rb new file mode 100644 index 000000000..3753d9d49 --- /dev/null +++ b/app/services/spot/s3_path.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +module Spot + # Path generators for the Valkyrie storage adapters. The default Valkyrie-Shrine + # adapter combines the resource id with a generated uuid to prevent accidental + # overwriting of files, but we're not concerned with that w/r/t source objects + # for media playback; we just want the most recent derivative. + # + # This follows the API set with IdPathGenerator. As a hack, to generate a pathname + # from a resource without an original filename available (say you're trying to access + # an IIIF tif file but the original file is a jpg), passing a glob string to the + # :original_filename parameter will use whatever extension is provided. + # + # @example Delete an existing IIIF access copy for a file_set + # file_set = Hyrax.query_service.find_by_alternate_identifier(alternate_identifier: 'file_set__1') + # s3_identifier = Spot::S3Path::IiifPathGenerator.new.generate(resource: file_set, original_filename: '*.tif') + # adapter = Valkyrie::StorageAdapter.find(:iiif_source_s3) + # adapter.delete(id: s3_identifier) + # + # @see https://github.com/samvera-labs/valkyrie-shrine/blob/v1.0.0/lib/valkyrie/storage/shrine.rb#L14-L30 + # @see config/initializers/hyrax.rb + module S3Path + class Base + def initialize(base_path: nil); end + end + + # IIIF source images are created on disk before sending to S3, + # so the desired filename has already been generated. + class IiifPathGenerator < Base + def generate(original_filename:, **) + original_filename + end + end + + # @todo get this from Jenn's work + class AvPathGenerator < Base + def generate(resource:, file:, original_filename:); end + end + end +end \ No newline at end of file diff --git a/config/initializers/hyrax.rb b/config/initializers/hyrax.rb index 52483636e..43e034253 100644 --- a/config/initializers/hyrax.rb +++ b/config/initializers/hyrax.rb @@ -20,6 +20,14 @@ config.solr_default_method = :post + # Use our own FileSetDerivativesService first and fall back to the Hyrax services + # for formats we don't currently handle uniquely. + config.derivative_services = [ + Spot::Derivatives::ImageDerivativeService, + Spot::Derivatives::BaseDerivativeService, + Hyrax::FileSetDerivativesService + ] + # Register roles that are expected by your implementation. # @see Hyrax::RoleRegistry for additional details. # @note there are magical roles as defined in Hyrax::RoleRegistry::MAGIC_ROLES diff --git a/config/initializers/spot_overrides.rb b/config/initializers/spot_overrides.rb index df38167d5..f61f5d358 100644 --- a/config/initializers/spot_overrides.rb +++ b/config/initializers/spot_overrides.rb @@ -26,13 +26,6 @@ Hyrax::CurationConcern.actor_factory.swap(Hyrax::Actors::CollectionsMembershipActor, Spot::Actors::CollectionsMembershipActor) - # Use our own FileSetDerivativesService first and fall back to the Hyrax services - # for formats we don't currently handle uniquely. - Hyrax::DerivativeService.services = [ - ::Spot::FileSetDerivativesService, - ::Hyrax::FileSetDerivativesService - ] - # Change the layout used for pages and the contact form Hyrax::ContactFormController.class_eval { layout 'hyrax/1_column' } Hyrax::PagesController.class_eval { layout 'hyrax/1_column' } diff --git a/config/initializers/wings.rb b/config/initializers/wings.rb index 72277e633..5850a3434 100644 --- a/config/initializers/wings.rb +++ b/config/initializers/wings.rb @@ -12,7 +12,6 @@ # from dassie: # "we register itself so we can pre-translate the class in Freyja instead of having to translate in each query_service" Wings::ModelRegistry.register(work_type, work_type) - end # Map AdminSets and Collections @@ -27,6 +26,11 @@ Wings::ModelRegistry.register(Hyrax::FileMetadata, Hydra::PCDM::File) Wings::ModelRegistry.register(Hydra::PCDM::File, Hydra::PCDM::File) + ## + # ADAPTERS SETUP + # metadata, indexing, storage + ## + Valkyrie::MetadataAdapter.register(Freyja::MetadataAdapter.new, :freyja) Valkyrie.config.metadata_adapter = :freyja @@ -39,6 +43,37 @@ ) Valkyrie.config.storage_adapter = :disk + # Use valkyrie-shrine's s3 capabilities to store iiif source images as a way + # to use more Samvera-community code rather than rolling our own AWS client usage. + # + # @see app/services/spot/derivatives/image_derivative_service.rb (:s3_iiif) + # @see https://github.com/samvera-labs/valkyrie-shrine/ + aws_opts = { force_path_style: !Rails.env.production? } + + Shrine.storages = { + s3_iiif: Shrine::Storage::S3.new(bucket: ENV.fetch('AWS_IIIF_ASSET_BUCKET'), **aws_opts), + s3_av: Shrine::Storage::S3.new(bucket: ENV.fetch('AWS_AV_ASSET_BUCKET'), **aws_opts) + } + + # @note We need to use a custom PathGenerator for the valkyrie-shrine adapter, as the default one + # appends a uuid to the path to prevent overwrites, but as these are access derivatives, we're not + # particularly concerned about that. + # + # @see app/services/spot/s3_path.rb + Valkyrie::StorageAdapter.register( + Valkyrie::Storage::Shrine.new(Shrine.storages[:s3_iiif], nil, Spot::S3Path::IiifPathGenerator), + :iiif_source_s3 + ) + + Valkyrie::StorageAdapter.register( + Valkyrie::Storage::Shrine.new(Shrine.storages[:s3_av], nil, Spot::S3Path::AvPathGenerator), + :av_source_s3 + ) + + # The :solr_index adapter is set up in a Hyrax initializer, so we just need to ensure + # that Hyrax and Valkyrie are configured to use it + # + # @see https://github.com/samvera/hyrax/blob/hyrax-v5.2.0/config/initializers/indexing_adapter_initializer.rb Hyrax.config.query_index_from_valkyrie = true Hyrax.config.index_adapter = :solr_index Valkyrie.config.indexing_adapter = :solr_index @@ -66,11 +101,12 @@ end Rails.application.config.to_prepare do - # Copied from + # Copied from Dassie but modified to map our CurationConcern work types to Hyrax::Resource classes Valkyrie.config.resource_class_resolver = lambda do |resource_klass_name| + resource_types = Hyrax.config.curation_concerns.map(&:to_s).concat(['Collection', 'AdminSet']) + klass_name = resource_klass_name.gsub(/^Wings\((.+)\)$/, '\1') klass_name = klass_name.gsub(/Resource$/, '') - resource_types = Hyrax.config.curation_concerns.map(&:to_s).concat(['Collection', 'AdminSet']) if resource_types.include?(klass_name) "#{klass_name}Resource".constantize From 9d6d128e40a551dbb804422f85847cd09dc6d55b Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Tue, 16 Jun 2026 15:20:06 -0400 Subject: [PATCH 13/38] cleanup + pcdm_members patch update from hyrax/main --- Dockerfile | 5 +++-- ...patch_for_pcdm_member_presenters_factory.rb | 7 +------ .../derivatives/base_derivative_service.rb | 2 +- config/application.rb | 1 - config/initializers/wings.rb | 18 +++++++++--------- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1e77d05d0..5ae90e3c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,7 +34,7 @@ ENV HYRAX_CACHE_PATH=/spot/tmp/cache \ HYRAX_UPLOAD_PATH=/spot/tmp/uploads \ BUNDLE_FORCE_RUBY_PLATFORM=1 -RUN corepack enable +RUN corepack enable yarn COPY Gemfile Gemfile.lock /spot/ RUN gem install bundler:$(tail -n 1 Gemfile.lock | sed -e 's/\s*//') @@ -61,7 +61,8 @@ FROM spot-base AS spot-asset-builder ENV RAILS_ENV=production COPY . /spot -RUN SECRET_KEY_BASE="$(bin/rake secret)" FEDORA_URL="http://fakehost:8080/rest" bundle exec rake assets:precompile +RUN echo y | yarn install \ + && SECRET_KEY_BASE="$(bin/rake secret)" FEDORA_URL="http://fakehost:8080/rest" bundle exec rake assets:precompile ## # TARGET: pdfjs-installer diff --git a/app/presenters/concerns/spot/lucene_patch_for_pcdm_member_presenters_factory.rb b/app/presenters/concerns/spot/lucene_patch_for_pcdm_member_presenters_factory.rb index 34a59bbd6..7787c353a 100644 --- a/app/presenters/concerns/spot/lucene_patch_for_pcdm_member_presenters_factory.rb +++ b/app/presenters/concerns/spot/lucene_patch_for_pcdm_member_presenters_factory.rb @@ -11,12 +11,7 @@ module LucenePatchForPcdmMemberPresentersFactory def query_docs(generic_type: nil, ids: object.member_ids) query = "{!terms f=id}#{ids.join(',')}" - - if generic_type - query += "{!term f=generic_type_si}#{generic_type}" - # works created via ActiveFedora use the _sim field - query += "{!term f=generic_type_sim}#{generic_type}" - end + query = "(generic_type_si:#{generic_type} OR generic_type_sim:#{generic_type}) AND #{query}" if generic_type Hyrax::SolrService .post(q: query, rows: 10_000, defType: 'lucene') diff --git a/app/services/spot/derivatives/base_derivative_service.rb b/app/services/spot/derivatives/base_derivative_service.rb index 60f9dc9c8..88f56969d 100644 --- a/app/services/spot/derivatives/base_derivative_service.rb +++ b/app/services/spot/derivatives/base_derivative_service.rb @@ -43,7 +43,7 @@ module Derivatives # # Hyrax.config.derivative_services = [ # CoolCustomDerivativeService, - # Spot::Derivatives::BaseDerivativeService + # Spot::Derivatives::BaseDerivativeService, # Hyrax::FileSetDerivativesService # ] # diff --git a/config/application.rb b/config/application.rb index c8b47f962..392858c31 100644 --- a/config/application.rb +++ b/config/application.rb @@ -73,6 +73,5 @@ def self.work_types end end end - end end diff --git a/config/initializers/wings.rb b/config/initializers/wings.rb index 5850a3434..d18db68bd 100644 --- a/config/initializers/wings.rb +++ b/config/initializers/wings.rb @@ -17,12 +17,12 @@ # Map AdminSets and Collections Hyrax::ValkyrieLazyMigration.migrating(AdminSetResource, from: ::AdminSet) Hyrax::ValkyrieLazyMigration.migrating(CollectionResource, from: ::Collection) + Hyrax::ValkyrieLazyMigration.migrating(Hyrax::FileSet, from: ::FileSet) Wings::ModelRegistry.register(AdminSet, AdminSet) Wings::ModelRegistry.register(Collection, Collection) Wings::ModelRegistry.register(FileSet, FileSet) - Wings::ModelRegistry.register(Hyrax::FileSet, FileSet) Wings::ModelRegistry.register(Hyrax::FileMetadata, Hydra::PCDM::File) Wings::ModelRegistry.register(Hydra::PCDM::File, Hydra::PCDM::File) @@ -104,23 +104,23 @@ # Copied from Dassie but modified to map our CurationConcern work types to Hyrax::Resource classes Valkyrie.config.resource_class_resolver = lambda do |resource_klass_name| resource_types = Hyrax.config.curation_concerns.map(&:to_s).concat(['Collection', 'AdminSet']) - klass_name = resource_klass_name.gsub(/^Wings\((.+)\)$/, '\1') klass_name = klass_name.gsub(/Resource$/, '') - if resource_types.include?(klass_name) - "#{klass_name}Resource".constantize + next "#{klass_name}Resource".constantize if resource_types.include?(klass_name) + + case klass_name + when 'Hydra::AccessControl' # Without this mapping, we'll see cases of Postgres Valkyrie adapter attempting to write to # Fedora. Yeah! - elsif 'Hydra::AccessControl' == klass_name Hyrax::AccessControl - elsif 'FileSet' == klass_name + when 'FileSet' Hyrax::FileSet - elsif 'Hydra::AccessControls::Embargo' == klass_name + when 'Hydra::AccessControls::Embargo' Hyrax::Embargo - elsif 'Hydra::AccessControls::Lease' == klass_name + when 'Hydra::AccessControls::Lease' Hyrax::Lease - elsif 'Hydra::PCDM::File' == klass_name + when 'Hydra::PCDM::File' Hyrax::FileMetadata else klass_name.constantize From 2602510fc4472f79eb2e553e57154616f236fc49 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Tue, 19 May 2026 16:36:44 -0400 Subject: [PATCH 14/38] hooking up to an av specific form --- app/controllers/hyrax/audio_visuals_controller.rb | 2 +- app/forms/audio_visual_resource_form.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 app/forms/audio_visual_resource_form.rb diff --git a/app/controllers/hyrax/audio_visuals_controller.rb b/app/controllers/hyrax/audio_visuals_controller.rb index 146f5e156..9cacb18b9 100644 --- a/app/controllers/hyrax/audio_visuals_controller.rb +++ b/app/controllers/hyrax/audio_visuals_controller.rb @@ -4,7 +4,7 @@ class AudioVisualsController < ApplicationController include ::Spot::WorksControllerBehavior # self.curation_concern_type = ::AudioVisual - self.curation_concern_type = ::PublicationResource + self.curation_concern_type = ::AudioVisualResource self.work_form_service = Hyrax::FormFactory.new self.show_presenter = Hyrax::AudioVisualPresenter diff --git a/app/forms/audio_visual_resource_form.rb b/app/forms/audio_visual_resource_form.rb new file mode 100644 index 000000000..913c66e65 --- /dev/null +++ b/app/forms/audio_visual_resource_form.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +class AudioVisualResourceForm < Hyrax::Forms::ResourceForm(AudioVisualResource) + + include Hyrax::FormFields(:core_metadata) + include Hyrax::FormFields(:base_metadata) + include Hyrax::FormFields(:audio_visual_metadata) +end From 7e566a1a5526c7f070996d9b12140da107891b2f Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Tue, 19 May 2026 16:53:19 -0400 Subject: [PATCH 15/38] basic form for all models --- app/controllers/hyrax/images_controller.rb | 2 +- app/controllers/hyrax/student_works_controller.rb | 2 +- app/forms/image_resource_form.rb | 6 ++++++ app/forms/student_work_resource_form.rb | 7 +++++++ 4 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 app/forms/image_resource_form.rb create mode 100644 app/forms/student_work_resource_form.rb diff --git a/app/controllers/hyrax/images_controller.rb b/app/controllers/hyrax/images_controller.rb index 3a34b09c5..391165a72 100644 --- a/app/controllers/hyrax/images_controller.rb +++ b/app/controllers/hyrax/images_controller.rb @@ -4,7 +4,7 @@ class ImagesController < ApplicationController include ::Spot::WorksControllerBehavior # self.curation_concern_type = ::Image - self.curation_concern_type = ::PublicationResource + self.curation_concern_type = ::ImageResource self.work_form_service = Hyrax::FormFactory.new self.show_presenter = Hyrax::ImagePresenter diff --git a/app/controllers/hyrax/student_works_controller.rb b/app/controllers/hyrax/student_works_controller.rb index 27c0de19a..0c3c7d32e 100644 --- a/app/controllers/hyrax/student_works_controller.rb +++ b/app/controllers/hyrax/student_works_controller.rb @@ -4,7 +4,7 @@ class StudentWorksController < ApplicationController include Spot::WorksControllerBehavior # self.curation_concern_type = ::StudentWork - self.curation_concern_type = ::PublicationResource + self.curation_concern_type = ::StudentWorkResource self.work_form_service = Hyrax::FormFactory.new self.show_presenter = Hyrax::StudentWorkPresenter diff --git a/app/forms/image_resource_form.rb b/app/forms/image_resource_form.rb new file mode 100644 index 000000000..648b45b7f --- /dev/null +++ b/app/forms/image_resource_form.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +class ImageResourceForm < Hyrax::Forms::ResourceForm(ImageResource) + include Hyrax::FormFields(:core_metadata) + include Hyrax::FormFields(:base_metadata) + include Hyrax::FormFields(:image_metadata) +end diff --git a/app/forms/student_work_resource_form.rb b/app/forms/student_work_resource_form.rb new file mode 100644 index 000000000..aa9143a9d --- /dev/null +++ b/app/forms/student_work_resource_form.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +class StudentWorkResourceForm < Hyrax::Forms::ResourceForm(StudentWorkResource) + include Hyrax::FormFields(:core_metadata) + include Hyrax::FormFields(:base_metadata) + include Hyrax::FormFields(:Student_work_metadata) + include Hyrax::FormFields(:institutional_metadata) +end From 68845500cadcb8dc75fe87d5bfc24aea1a0a6aa9 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Thu, 21 May 2026 12:50:30 -0400 Subject: [PATCH 16/38] multi authority input code from legacy --- .../multi_authority_controlled_vocabulary_input.rb | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/inputs/multi_authority_controlled_vocabulary_input.rb b/app/inputs/multi_authority_controlled_vocabulary_input.rb index a80e199a9..257fa71bd 100644 --- a/app/inputs/multi_authority_controlled_vocabulary_input.rb +++ b/app/inputs/multi_authority_controlled_vocabulary_input.rb @@ -77,14 +77,7 @@ def collection def collection_values val = object[attribute_name] col = val.respond_to?(:to_ary) ? val.to_ary : val - col.reject { |value| value.respond_to?(:node?) ? value.node? : value.to_s.strip.blank? } + [cv_klass.new] - end - - # class name of the controlled vocabulary for this property - # - # @return [Class] - def cv_klass - object.model.class.properties[attribute_name.to_s].class_name + col.reject { |value| value.try(:node?) == true } end def id_for_select(index) From 0ef5ac68a2c37aa5cd628ef4886e88b7d484c513 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Tue, 26 May 2026 15:31:41 -0400 Subject: [PATCH 17/38] preliminary forms --- app/forms/audio_visual_resource_form.rb | 36 +++++++++++++++++++++++++ app/forms/image_resource_form.rb | 36 +++++++++++++++++++++++++ app/forms/publication_resource_form.rb | 2 +- app/forms/student_work_resource_form.rb | 31 +++++++++++++++++++++ 4 files changed, 104 insertions(+), 1 deletion(-) diff --git a/app/forms/audio_visual_resource_form.rb b/app/forms/audio_visual_resource_form.rb index 913c66e65..3b0acd5ce 100644 --- a/app/forms/audio_visual_resource_form.rb +++ b/app/forms/audio_visual_resource_form.rb @@ -4,4 +4,40 @@ class AudioVisualResourceForm < Hyrax::Forms::ResourceForm(AudioVisualResource) include Hyrax::FormFields(:core_metadata) include Hyrax::FormFields(:base_metadata) include Hyrax::FormFields(:audio_visual_metadata) + + def primary_terms + [ + # required_fields first + :title, + :date, + :resource_type, + :rights_statement, + + # non-required fields + :rights_holder, + :subtitle, + :title_alternative, + :date_associated, + :creator, + :contributor, + :publisher, + :source, + :standard_identifier, + :local_identifier, + :description, + :inscription, + :subject, + :keyword, + :language, + :physical_medium, + :original_item_extent, + :location, + :repository_location, + :note, + :related_resource, + :research_assistance, + :provenance, + :barcode + ] + end end diff --git a/app/forms/image_resource_form.rb b/app/forms/image_resource_form.rb index 648b45b7f..ae0b659b6 100644 --- a/app/forms/image_resource_form.rb +++ b/app/forms/image_resource_form.rb @@ -3,4 +3,40 @@ class ImageResourceForm < Hyrax::Forms::ResourceForm(ImageResource) include Hyrax::FormFields(:core_metadata) include Hyrax::FormFields(:base_metadata) include Hyrax::FormFields(:image_metadata) + + def primary_terms + [ + :title, + :date, + :resource_type, + :rights_statement, + + # non-required fields + :title_alternative, + :subtitle, + :date_associated, + :date_scope_note, + :rights_holder, + :description, + :inscription, + :creator, + :contributor, + :publisher, + :keyword, + :subject, + :location, + :language, + :source, + :physical_medium, + :original_item_extent, + :repository_location, + :requested_by, + :research_assistance, + :donor, + :related_resource, + :local_identifier, + :subject_ocm, + :note + ] + end end diff --git a/app/forms/publication_resource_form.rb b/app/forms/publication_resource_form.rb index 91fbc0ebd..e1a272003 100644 --- a/app/forms/publication_resource_form.rb +++ b/app/forms/publication_resource_form.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class PublicationResourceForm < Hyrax::Forms::ResourceForm(PublicationResource) - include Spot::Forms::BaseResourceFormBehavior + #include Spot::Forms::BaseResourceFormBehavior include Hyrax::FormFields(:publication_metadata) include Hyrax::FormFields(:institutional_metadata) diff --git a/app/forms/student_work_resource_form.rb b/app/forms/student_work_resource_form.rb index aa9143a9d..e1fa63fd0 100644 --- a/app/forms/student_work_resource_form.rb +++ b/app/forms/student_work_resource_form.rb @@ -4,4 +4,35 @@ class StudentWorkResourceForm < Hyrax::Forms::ResourceForm(StudentWorkResource) include Hyrax::FormFields(:base_metadata) include Hyrax::FormFields(:Student_work_metadata) include Hyrax::FormFields(:institutional_metadata) + + def primary_terms + [ + :title, + :creator, + :advisor, + :academic_department, + :description, + :date, + :date_available, + :resource_type, + :rights_statement, + :rights_holder + ] + end + + def secondary_terms + [ + :division, + :abstract, + :language, + :related_resource, + :organization, + :subject, + :keyword, + :bibliographic_citation, + :standard_identifier, + :access_note, + :note + ] + end end From f10788b6f09945384bfa27f45a1ef1b0c31c0e98 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Wed, 27 May 2026 10:38:31 -0400 Subject: [PATCH 18/38] date parse fix --- app/forms/publication_resource_form.rb | 2 +- app/indexers/base_resource_indexer.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/forms/publication_resource_form.rb b/app/forms/publication_resource_form.rb index e1a272003..91fbc0ebd 100644 --- a/app/forms/publication_resource_form.rb +++ b/app/forms/publication_resource_form.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class PublicationResourceForm < Hyrax::Forms::ResourceForm(PublicationResource) - #include Spot::Forms::BaseResourceFormBehavior + include Spot::Forms::BaseResourceFormBehavior include Hyrax::FormFields(:publication_metadata) include Hyrax::FormFields(:institutional_metadata) diff --git a/app/indexers/base_resource_indexer.rb b/app/indexers/base_resource_indexer.rb index 55c6f87f4..082cfdc1a 100644 --- a/app/indexers/base_resource_indexer.rb +++ b/app/indexers/base_resource_indexer.rb @@ -77,7 +77,7 @@ def parse_sortable_date parsed = Date.edtf(value) parsed = parsed.first if parsed.class < ::Enumerable # guard for EDTF sets/intervals/etc - parsed ||= Date.parse(resource.create_date.to_s) + parsed ||= Date.parse(resource.created_at.to_s) parsed.strftime('%FT%TZ') end From b64512b7e862dcd2f07e20abf199adc1a2fd5ee3 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Thu, 28 May 2026 13:22:35 -0400 Subject: [PATCH 19/38] not currently working --- app/forms/audio_visual_resource_form.rb | 4 +++- app/forms/image_resource_form.rb | 4 ++++ app/forms/student_work_resource_form.rb | 4 ++++ app/models/audio_visual_resource.rb | 8 ++++++++ app/models/image_resource.rb | 8 ++++++++ app/models/student_work_resource.rb | 8 ++++++++ 6 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/forms/audio_visual_resource_form.rb b/app/forms/audio_visual_resource_form.rb index 3b0acd5ce..87fefff11 100644 --- a/app/forms/audio_visual_resource_form.rb +++ b/app/forms/audio_visual_resource_form.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true class AudioVisualResourceForm < Hyrax::Forms::ResourceForm(AudioVisualResource) + include Spot::Forms::BaseResourceFormBehavior include Hyrax::FormFields(:core_metadata) include Hyrax::FormFields(:base_metadata) include Hyrax::FormFields(:audio_visual_metadata) + language_tagged_field(:inscription) + def primary_terms [ # required_fields first @@ -22,7 +25,6 @@ def primary_terms :contributor, :publisher, :source, - :standard_identifier, :local_identifier, :description, :inscription, diff --git a/app/forms/image_resource_form.rb b/app/forms/image_resource_form.rb index ae0b659b6..db47b5923 100644 --- a/app/forms/image_resource_form.rb +++ b/app/forms/image_resource_form.rb @@ -1,9 +1,13 @@ # frozen_string_literal: true class ImageResourceForm < Hyrax::Forms::ResourceForm(ImageResource) + include Spot::Forms::BaseResourceFormBehavior + include Hyrax::FormFields(:core_metadata) include Hyrax::FormFields(:base_metadata) include Hyrax::FormFields(:image_metadata) + language_tagged_field(:inscription) + def primary_terms [ :title, diff --git a/app/forms/student_work_resource_form.rb b/app/forms/student_work_resource_form.rb index e1fa63fd0..bba601a14 100644 --- a/app/forms/student_work_resource_form.rb +++ b/app/forms/student_work_resource_form.rb @@ -1,10 +1,14 @@ # frozen_string_literal: true class StudentWorkResourceForm < Hyrax::Forms::ResourceForm(StudentWorkResource) + include Spot::Forms::BaseResourceFormBehavior + include Hyrax::FormFields(:core_metadata) include Hyrax::FormFields(:base_metadata) include Hyrax::FormFields(:Student_work_metadata) include Hyrax::FormFields(:institutional_metadata) + nested_attributes_for(:academic_department, :division, :advisor) + def primary_terms [ :title, diff --git a/app/models/audio_visual_resource.rb b/app/models/audio_visual_resource.rb index 9d3861949..632a25dc6 100644 --- a/app/models/audio_visual_resource.rb +++ b/app/models/audio_visual_resource.rb @@ -2,5 +2,13 @@ class AudioVisualResource < BaseResource include Hyrax::Schema(:audio_visual_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + def identifier + attributes[:identifier].map { |v| v.is_a?(String) ? Spot::Identifier.from_string(v) : v } + end + + def local_identifier + identifier.select(&:local?) + end + attribute :stored_derivatives, Valkyrie::Types::String end diff --git a/app/models/image_resource.rb b/app/models/image_resource.rb index 6a3685043..a4f7c9f20 100644 --- a/app/models/image_resource.rb +++ b/app/models/image_resource.rb @@ -1,4 +1,12 @@ # frozen_string_literal: true class ImageResource < BaseResource include Hyrax::Schema(:image_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + + def identifier + attributes[:identifier].map { |v| v.is_a?(String) ? Spot::Identifier.from_string(v) : v } + end + + def local_identifier + identifier.select(&:local?) + end end diff --git a/app/models/student_work_resource.rb b/app/models/student_work_resource.rb index 5cd1c7db2..0051ea9ca 100644 --- a/app/models/student_work_resource.rb +++ b/app/models/student_work_resource.rb @@ -2,4 +2,12 @@ class StudentWorkResource < BaseResource include Hyrax::Schema(:institutional_metadata, schema_loader: Spot::SimpleSchemaLoader.new) include Hyrax::Schema(:student_work_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + + def identifier + attributes[:identifier].map { |v| v.is_a?(String) ? Spot::Identifier.from_string(v) : v } + end + + def standard_identifier + identifier.select(&:standard?) + end end From d7bb7b8f9b3f8af8f6662ad5e40ded06358ea544 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Fri, 29 May 2026 11:25:04 -0400 Subject: [PATCH 20/38] id and activate object fixes --- app/forms/audio_visual_resource_form.rb | 2 -- app/forms/image_resource_form.rb | 2 -- app/forms/student_work_resource_form.rb | 4 +--- app/models/audio_visual_resource.rb | 4 ++++ app/models/image_resource.rb | 6 +++++- app/models/student_work_resource.rb | 6 +++++- app/services/spot/workflow/activate_object.rb | 14 ++++++++------ 7 files changed, 23 insertions(+), 15 deletions(-) diff --git a/app/forms/audio_visual_resource_form.rb b/app/forms/audio_visual_resource_form.rb index 87fefff11..5d9d9dff0 100644 --- a/app/forms/audio_visual_resource_form.rb +++ b/app/forms/audio_visual_resource_form.rb @@ -2,8 +2,6 @@ class AudioVisualResourceForm < Hyrax::Forms::ResourceForm(AudioVisualResource) include Spot::Forms::BaseResourceFormBehavior - include Hyrax::FormFields(:core_metadata) - include Hyrax::FormFields(:base_metadata) include Hyrax::FormFields(:audio_visual_metadata) language_tagged_field(:inscription) diff --git a/app/forms/image_resource_form.rb b/app/forms/image_resource_form.rb index db47b5923..f7a4b4bee 100644 --- a/app/forms/image_resource_form.rb +++ b/app/forms/image_resource_form.rb @@ -2,8 +2,6 @@ class ImageResourceForm < Hyrax::Forms::ResourceForm(ImageResource) include Spot::Forms::BaseResourceFormBehavior - include Hyrax::FormFields(:core_metadata) - include Hyrax::FormFields(:base_metadata) include Hyrax::FormFields(:image_metadata) language_tagged_field(:inscription) diff --git a/app/forms/student_work_resource_form.rb b/app/forms/student_work_resource_form.rb index bba601a14..49d1f9e29 100644 --- a/app/forms/student_work_resource_form.rb +++ b/app/forms/student_work_resource_form.rb @@ -2,9 +2,7 @@ class StudentWorkResourceForm < Hyrax::Forms::ResourceForm(StudentWorkResource) include Spot::Forms::BaseResourceFormBehavior - include Hyrax::FormFields(:core_metadata) - include Hyrax::FormFields(:base_metadata) - include Hyrax::FormFields(:Student_work_metadata) + include Hyrax::FormFields(:student_work_metadata) include Hyrax::FormFields(:institutional_metadata) nested_attributes_for(:academic_department, :division, :advisor) diff --git a/app/models/audio_visual_resource.rb b/app/models/audio_visual_resource.rb index 632a25dc6..db9ff6eeb 100644 --- a/app/models/audio_visual_resource.rb +++ b/app/models/audio_visual_resource.rb @@ -9,6 +9,10 @@ def identifier def local_identifier identifier.select(&:local?) end + + def standard_identifier + identifier.select(&:standard?) + end attribute :stored_derivatives, Valkyrie::Types::String end diff --git a/app/models/image_resource.rb b/app/models/image_resource.rb index a4f7c9f20..b8b6687f0 100644 --- a/app/models/image_resource.rb +++ b/app/models/image_resource.rb @@ -2,11 +2,15 @@ class ImageResource < BaseResource include Hyrax::Schema(:image_metadata, schema_loader: Spot::SimpleSchemaLoader.new) - def identifier + def identifier attributes[:identifier].map { |v| v.is_a?(String) ? Spot::Identifier.from_string(v) : v } end def local_identifier identifier.select(&:local?) end + + def standard_identifier + identifier.select(&:standard?) + end end diff --git a/app/models/student_work_resource.rb b/app/models/student_work_resource.rb index 0051ea9ca..72464bd74 100644 --- a/app/models/student_work_resource.rb +++ b/app/models/student_work_resource.rb @@ -3,10 +3,14 @@ class StudentWorkResource < BaseResource include Hyrax::Schema(:institutional_metadata, schema_loader: Spot::SimpleSchemaLoader.new) include Hyrax::Schema(:student_work_metadata, schema_loader: Spot::SimpleSchemaLoader.new) - def identifier + def identifier attributes[:identifier].map { |v| v.is_a?(String) ? Spot::Identifier.from_string(v) : v } end + def local_identifier + identifier.select(&:local?) + end + def standard_identifier identifier.select(&:standard?) end diff --git a/app/services/spot/workflow/activate_object.rb b/app/services/spot/workflow/activate_object.rb index 14ec9abd9..0929e3e7d 100644 --- a/app/services/spot/workflow/activate_object.rb +++ b/app/services/spot/workflow/activate_object.rb @@ -20,13 +20,15 @@ def self.call(target:, **kwargs) Hyrax::Workflow::ActivateObject.call(target: target, **kwargs) return true if target.respond_to?(:date_available) && target.date_available.present? - date = if target.try(:embargo) && target.embargo.try(:embargo_release_date).present? - target.embargo.embargo_release_date - else - Time.zone.now - end + if target.respond_to?(:date_available=) && target.date_available.blank? + date = if target.try(:embargo) && target.embargo.try(:embargo_release_date).present? + target.embargo.embargo_release_date + else + Time.zone.now + end - target.date_available = [date.strftime('%Y-%m-%d')] + target.date_available = [date.strftime('%Y-%m-%d')] + end true end From 7d8f393a3566362520ada53e825a51aab1bbff24 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Mon, 1 Jun 2026 15:36:54 -0400 Subject: [PATCH 21/38] temporary solution for form field duplication --- app/forms/audio_visual_resource_form.rb | 4 ++++ app/forms/image_resource_form.rb | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/forms/audio_visual_resource_form.rb b/app/forms/audio_visual_resource_form.rb index 5d9d9dff0..7eed340f6 100644 --- a/app/forms/audio_visual_resource_form.rb +++ b/app/forms/audio_visual_resource_form.rb @@ -40,4 +40,8 @@ def primary_terms :barcode ] end + + def secondary_terms + [] + end end diff --git a/app/forms/image_resource_form.rb b/app/forms/image_resource_form.rb index f7a4b4bee..b8cc6f361 100644 --- a/app/forms/image_resource_form.rb +++ b/app/forms/image_resource_form.rb @@ -41,4 +41,8 @@ def primary_terms :note ] end + + def secondary_terms + [] + end end From ed07da09393418a9e73562aabe9b6db4baa7f807 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Tue, 2 Jun 2026 10:04:02 -0400 Subject: [PATCH 22/38] undo --- app/forms/audio_visual_resource_form.rb | 4 ---- app/forms/image_resource_form.rb | 4 ---- 2 files changed, 8 deletions(-) diff --git a/app/forms/audio_visual_resource_form.rb b/app/forms/audio_visual_resource_form.rb index 7eed340f6..5d9d9dff0 100644 --- a/app/forms/audio_visual_resource_form.rb +++ b/app/forms/audio_visual_resource_form.rb @@ -40,8 +40,4 @@ def primary_terms :barcode ] end - - def secondary_terms - [] - end end diff --git a/app/forms/image_resource_form.rb b/app/forms/image_resource_form.rb index b8cc6f361..f7a4b4bee 100644 --- a/app/forms/image_resource_form.rb +++ b/app/forms/image_resource_form.rb @@ -41,8 +41,4 @@ def primary_terms :note ] end - - def secondary_terms - [] - end end From b883f1a30290d149ab0cb9127270e163bb159693 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Tue, 2 Jun 2026 15:42:34 -0400 Subject: [PATCH 23/38] all metadata is primary --- config/metadata/audio_visual_metadata.yaml | 8 ++++++++ config/metadata/base_metadata.yaml | 17 +++++++++++++++++ config/metadata/image_metadata.yaml | 10 ++++++++++ config/metadata/institutional_metadata.yaml | 3 +++ config/metadata/publication_metadata.yaml | 4 ++++ config/metadata/student_work_metadata.yaml | 5 +++++ 6 files changed, 47 insertions(+) diff --git a/config/metadata/audio_visual_metadata.yaml b/config/metadata/audio_visual_metadata.yaml index 0e2535856..7fe7adfec 100644 --- a/config/metadata/audio_visual_metadata.yaml +++ b/config/metadata/audio_visual_metadata.yaml @@ -5,6 +5,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - barcode_ssim date: @@ -14,6 +15,7 @@ attributes: required: true form: multiple: true + primary: true index_keys: - date_ssim date_associated: @@ -22,6 +24,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - date_associated_ssim - date_associated_tesim @@ -31,6 +34,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - inscription_tesim original_item_extent: @@ -39,6 +43,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - original_item_extent_tesim repository_location: @@ -47,6 +52,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - repository_location_ssim research_assistance: @@ -55,6 +61,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - research_assistance_ssim provenance: @@ -63,5 +70,6 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - provenance_tesim diff --git a/config/metadata/base_metadata.yaml b/config/metadata/base_metadata.yaml index 4283f6dae..5eaf30f54 100644 --- a/config/metadata/base_metadata.yaml +++ b/config/metadata/base_metadata.yaml @@ -5,6 +5,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - bibliographic_citation_tesim contributor: @@ -13,6 +14,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - contributor_tesim - contributor_sim @@ -22,6 +24,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - creator_tesim - creator_sim @@ -40,6 +43,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - identifier_ssim keyword: @@ -48,6 +52,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - keyword_tesim - keyword_sim @@ -57,6 +62,7 @@ attributes: multiple: true form: multiple: true + primary: true # @note RDF indexing for :location is set up in app/models/base_resource.rb # and uses "location_ssim" and "location_label_ssim" keys location: @@ -65,12 +71,14 @@ attributes: multiple: true form: multiple: true + primary: true note: predicate: http://www.w3.org/2004/02/skos/core#note type: string multiple: true form: multiple: true + primary: true index_keys: - note_tesim physical_medium: @@ -79,6 +87,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - physical_medium_tesim - physical_medium_sim @@ -88,6 +97,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - publisher_tesim - publisher_sim @@ -97,6 +107,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - related_resource_tesim - related_resource_sim @@ -107,6 +118,7 @@ attributes: form: multiple: true required: true + primary: true index_keys: - resource_type_ssim rights_holder: @@ -115,6 +127,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - rights_holder_tesim - rights_holder_sim @@ -125,6 +138,7 @@ attributes: form: multiple: false required: true + primary: true index_keys: - rights_statement_ssim source: @@ -133,6 +147,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - source_tesim - source_sim @@ -158,6 +173,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - subtitle_tesim - subtitle_sim @@ -167,6 +183,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - title_alternative_tesim - title_alternative_sim \ No newline at end of file diff --git a/config/metadata/image_metadata.yaml b/config/metadata/image_metadata.yaml index 9e409f5b7..54503b018 100644 --- a/config/metadata/image_metadata.yaml +++ b/config/metadata/image_metadata.yaml @@ -7,6 +7,7 @@ attributes: required: true form: multiple: true + primary: true index_keys: - date_ssim date_associated: @@ -15,6 +16,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - date_associated_ssim - date_associated_tesim @@ -24,6 +26,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - date_scope_note_tesim donor: @@ -32,6 +35,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - donor_ssim inscription: @@ -40,6 +44,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - inscription_tesim original_item_extent: @@ -48,6 +53,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - original_item_extent_tesim repository_location: @@ -56,6 +62,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - repository_location_ssim requested_by: @@ -64,6 +71,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - requested_by_ssim research_assistance: @@ -72,6 +80,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - research_assistance_ssim subject_ocm: @@ -80,6 +89,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - subject_ocm_tesim - subject_ocm_ssim diff --git a/config/metadata/institutional_metadata.yaml b/config/metadata/institutional_metadata.yaml index 5e3e9ea44..0d771b9b2 100644 --- a/config/metadata/institutional_metadata.yaml +++ b/config/metadata/institutional_metadata.yaml @@ -5,6 +5,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - academic_department_tesim - academic_department_sim @@ -14,6 +15,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - division_tesim - division_sim @@ -23,6 +25,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - organization_tesim - organization_sim \ No newline at end of file diff --git a/config/metadata/publication_metadata.yaml b/config/metadata/publication_metadata.yaml index d4c6fe2fc..95881d0b1 100644 --- a/config/metadata/publication_metadata.yaml +++ b/config/metadata/publication_metadata.yaml @@ -5,6 +5,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - abstract_tesim date_issued: @@ -14,6 +15,7 @@ attributes: form: multiple: false required: true + primary: true index_keys: - date_issued_ssim # @todo should this be a date field? @@ -23,6 +25,7 @@ attributes: multiple: true form: multiple: false + primary: true index_keys: - date_available_ssim editor: @@ -31,6 +34,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - editor_sim - editor_tesim diff --git a/config/metadata/student_work_metadata.yaml b/config/metadata/student_work_metadata.yaml index db9bcc5b6..f6fb2f0c4 100644 --- a/config/metadata/student_work_metadata.yaml +++ b/config/metadata/student_work_metadata.yaml @@ -5,6 +5,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - abstract_tesim access_note: @@ -13,6 +14,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - access_note_tesim advisor: @@ -21,6 +23,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - advisor_ssim - advisor_tesim @@ -32,6 +35,7 @@ attributes: required: true form: multiple: true + primary: true index_keys: - date_ssim date_available: @@ -40,5 +44,6 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - date_available_ssim From 40e20e285969a6237c1196de18843107f61b3843 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Fri, 12 Jun 2026 09:40:35 -0400 Subject: [PATCH 24/38] explicitly define indexing adapter --- config/initializers/wings.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/initializers/wings.rb b/config/initializers/wings.rb index d18db68bd..87e281002 100644 --- a/config/initializers/wings.rb +++ b/config/initializers/wings.rb @@ -33,6 +33,8 @@ Valkyrie::MetadataAdapter.register(Freyja::MetadataAdapter.new, :freyja) Valkyrie.config.metadata_adapter = :freyja + Hyrax.config.query_index_from_valkyrie = true + Hyrax.config.index_adapter = :solr_index Valkyrie::StorageAdapter.register( Valkyrie::Storage::VersionedDisk.new( @@ -42,6 +44,7 @@ :disk ) Valkyrie.config.storage_adapter = :disk + Valkyrie.config.indexing_adapter = :solr_index # Use valkyrie-shrine's s3 capabilities to store iiif source images as a way # to use more Samvera-community code rather than rolling our own AWS client usage. From aa1a3f1132c6401c1cedb08a54676fe452e79a59 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Fri, 12 Jun 2026 10:17:46 -0400 Subject: [PATCH 25/38] rubo 1 --- app/forms/audio_visual_resource_form.rb | 2 +- app/forms/image_resource_form.rb | 2 +- app/indexers/publication_resource_indexer.rb | 2 +- app/models/audio_visual_resource.rb | 2 +- app/models/concerns/spot/has_controlled_fields.rb | 1 + app/models/user.rb | 1 - app/services/spot/derivatives/base_derivative_service.rb | 2 +- app/services/spot/derivatives/image_derivative_service.rb | 2 +- 8 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/forms/audio_visual_resource_form.rb b/app/forms/audio_visual_resource_form.rb index 5d9d9dff0..a3f6e4443 100644 --- a/app/forms/audio_visual_resource_form.rb +++ b/app/forms/audio_visual_resource_form.rb @@ -6,7 +6,7 @@ class AudioVisualResourceForm < Hyrax::Forms::ResourceForm(AudioVisualResource) language_tagged_field(:inscription) - def primary_terms + def primary_terms # rubocop:disable Metrics/MethodLength [ # required_fields first :title, diff --git a/app/forms/image_resource_form.rb b/app/forms/image_resource_form.rb index f7a4b4bee..c72648aee 100644 --- a/app/forms/image_resource_form.rb +++ b/app/forms/image_resource_form.rb @@ -6,7 +6,7 @@ class ImageResourceForm < Hyrax::Forms::ResourceForm(ImageResource) language_tagged_field(:inscription) - def primary_terms + def primary_terms # rubocop:disable Metrics/MethodLength [ :title, :date, diff --git a/app/indexers/publication_resource_indexer.rb b/app/indexers/publication_resource_indexer.rb index 8e2e107b2..a962e0959 100644 --- a/app/indexers/publication_resource_indexer.rb +++ b/app/indexers/publication_resource_indexer.rb @@ -65,4 +65,4 @@ def season_names_for_date(date) def full_and_abbreviated_months_for_date(date) %w[%B %b].map { |month| date.strftime("#{month} %Y") } end -end \ No newline at end of file +end diff --git a/app/models/audio_visual_resource.rb b/app/models/audio_visual_resource.rb index db9ff6eeb..735c428e5 100644 --- a/app/models/audio_visual_resource.rb +++ b/app/models/audio_visual_resource.rb @@ -13,6 +13,6 @@ def local_identifier def standard_identifier identifier.select(&:standard?) end - + attribute :stored_derivatives, Valkyrie::Types::String end diff --git a/app/models/concerns/spot/has_controlled_fields.rb b/app/models/concerns/spot/has_controlled_fields.rb index fe59c48e7..e5b373d84 100644 --- a/app/models/concerns/spot/has_controlled_fields.rb +++ b/app/models/concerns/spot/has_controlled_fields.rb @@ -5,6 +5,7 @@ module HasControlledFields extend ActiveSupport::Concern module ClassMethods + # rubocop:disable Naming/PredicateName def has_controlled_field(field, vocabulary_class: ActiveTriples::Resource) controlled_fields << field unless controlled_fields.include?(field) diff --git a/app/models/user.rb b/app/models/user.rb index f789c014c..632cbcc00 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -30,7 +30,6 @@ def find_or_create_system_user(user_key) end end - # Does this user belong to the Alumni group? # # @return [true, false] diff --git a/app/services/spot/derivatives/base_derivative_service.rb b/app/services/spot/derivatives/base_derivative_service.rb index 88f56969d..a5fcac4c9 100644 --- a/app/services/spot/derivatives/base_derivative_service.rb +++ b/app/services/spot/derivatives/base_derivative_service.rb @@ -92,7 +92,7 @@ def create_thumbnail_from(path) # Copied from Hyrax::FileSetDerivativeService # # @see https://github.com/samvera/hyrax/blob/hyrax-v4.0.0/app/services/hyrax/file_set_derivatives_service.rb#L119-L127 - def extract_and_save_full_text(src_path) + def extract_and_save_full_text(_src_path) return unless Hyrax.config.extract_full_text? Rails.logger.warn 'Skipping full-text extraction for the moment' diff --git a/app/services/spot/derivatives/image_derivative_service.rb b/app/services/spot/derivatives/image_derivative_service.rb index 8ab526d16..e19b33bba 100644 --- a/app/services/spot/derivatives/image_derivative_service.rb +++ b/app/services/spot/derivatives/image_derivative_service.rb @@ -105,4 +105,4 @@ def working_directory end end end -end \ No newline at end of file +end From 477223a1e3cd5dca4d230205a17e27ce208cffa5 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Fri, 12 Jun 2026 10:38:40 -0400 Subject: [PATCH 26/38] rubo 2 --- app/services/spot/s3_path.rb | 2 +- app/services/spot/workflow/activate_object.rb | 12 ++++++------ spec/indexers/student_work_resource_indexer_spec.rb | 2 +- spec/models/image_resource_spec.rb | 1 - 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/services/spot/s3_path.rb b/app/services/spot/s3_path.rb index 3753d9d49..733787036 100644 --- a/app/services/spot/s3_path.rb +++ b/app/services/spot/s3_path.rb @@ -36,4 +36,4 @@ class AvPathGenerator < Base def generate(resource:, file:, original_filename:); end end end -end \ No newline at end of file +end diff --git a/app/services/spot/workflow/activate_object.rb b/app/services/spot/workflow/activate_object.rb index 0929e3e7d..738e62a30 100644 --- a/app/services/spot/workflow/activate_object.rb +++ b/app/services/spot/workflow/activate_object.rb @@ -21,13 +21,13 @@ def self.call(target:, **kwargs) return true if target.respond_to?(:date_available) && target.date_available.present? if target.respond_to?(:date_available=) && target.date_available.blank? - date = if target.try(:embargo) && target.embargo.try(:embargo_release_date).present? - target.embargo.embargo_release_date - else - Time.zone.now - end + if target.try(:embargo) && target.embargo.try(:embargo_release_date).present? + date = target.embargo.embargo_release_date + else + date = Time.zone.now + end - target.date_available = [date.strftime('%Y-%m-%d')] + target.date_available = [date.strftime('%Y-%m-%d')] end true diff --git a/spec/indexers/student_work_resource_indexer_spec.rb b/spec/indexers/student_work_resource_indexer_spec.rb index eb67448a6..820cb8d21 100644 --- a/spec/indexers/student_work_resource_indexer_spec.rb +++ b/spec/indexers/student_work_resource_indexer_spec.rb @@ -46,7 +46,7 @@ advisor: ['Professor, A'], access_note: ['upon request only'], date: ['2026-05-08'], - date_available: ['2026-05-08'], + date_available: ['2026-05-08'] } end diff --git a/spec/models/image_resource_spec.rb b/spec/models/image_resource_spec.rb index 00977dab2..15baa0f0d 100644 --- a/spec/models/image_resource_spec.rb +++ b/spec/models/image_resource_spec.rb @@ -67,6 +67,5 @@ .to change { resource.subject_ocm } .to contain_exactly('000 VALUE') end - end end \ No newline at end of file From 0108b935a4b37d8f8a6f368394c71d138c581c56 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Fri, 12 Jun 2026 11:29:13 -0400 Subject: [PATCH 27/38] rubo 3 --- app/services/spot/workflow/activate_object.rb | 10 +++++----- spec/models/image_resource_spec.rb | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/services/spot/workflow/activate_object.rb b/app/services/spot/workflow/activate_object.rb index 738e62a30..6cf254591 100644 --- a/app/services/spot/workflow/activate_object.rb +++ b/app/services/spot/workflow/activate_object.rb @@ -21,11 +21,11 @@ def self.call(target:, **kwargs) return true if target.respond_to?(:date_available) && target.date_available.present? if target.respond_to?(:date_available=) && target.date_available.blank? - if target.try(:embargo) && target.embargo.try(:embargo_release_date).present? - date = target.embargo.embargo_release_date - else - date = Time.zone.now - end + date = if target.try(:embargo) && target.embargo.try(:embargo_release_date).present? + target.embargo.embargo_release_date + else + Time.zone.now + end target.date_available = [date.strftime('%Y-%m-%d')] end diff --git a/spec/models/image_resource_spec.rb b/spec/models/image_resource_spec.rb index 15baa0f0d..516b7731c 100644 --- a/spec/models/image_resource_spec.rb +++ b/spec/models/image_resource_spec.rb @@ -68,4 +68,4 @@ .to contain_exactly('000 VALUE') end end -end \ No newline at end of file +end From 75be263f549f446c7aa849c03b6a4bb30dd59fb6 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Fri, 12 Jun 2026 11:43:32 -0400 Subject: [PATCH 28/38] rubo 4 mostly giving up on this one --- app/indexers/base_resource_indexer.rb | 4 +++- app/services/spot/workflow/activate_object.rb | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/indexers/base_resource_indexer.rb b/app/indexers/base_resource_indexer.rb index 082cfdc1a..f7483c06f 100644 --- a/app/indexers/base_resource_indexer.rb +++ b/app/indexers/base_resource_indexer.rb @@ -41,6 +41,7 @@ def to_solr private + # rubocop:disable Metrics/CyclomaticComplexity def citation_metadata return {} unless resource.respond_to?(:bibliographic_citation) && resource.bibliographic_citation.present? @@ -85,10 +86,11 @@ def parse_sortable_date # "Years Encompassed" meaning what years are covered by the metadata dates for a resource. # Handles individual dates and EDTF ranges (so "2001/2003" encompasses "2001", "2002", "2003"). # Used for the blacklight_range_limit plugin. - def parse_years_encompassed + def parse_years_encompassed fields = Array.wrap(years_encompassed_fields) return [] unless fields.any? { |field| resource.respond_to?(field) } + # rubocop:disable Style/BlockDelimiters fields.map { |f| resource.try(f).try(:to_a) || [] } .flatten .reduce([]) { |dates, date| diff --git a/app/services/spot/workflow/activate_object.rb b/app/services/spot/workflow/activate_object.rb index 6cf254591..891a6820d 100644 --- a/app/services/spot/workflow/activate_object.rb +++ b/app/services/spot/workflow/activate_object.rb @@ -21,11 +21,11 @@ def self.call(target:, **kwargs) return true if target.respond_to?(:date_available) && target.date_available.present? if target.respond_to?(:date_available=) && target.date_available.blank? - date = if target.try(:embargo) && target.embargo.try(:embargo_release_date).present? - target.embargo.embargo_release_date - else - Time.zone.now - end + date= if target.try(:embargo) && target.embargo.try(:embargo_release_date).present? + target.embargo.embargo_release_date + else + Time.zone.now + end target.date_available = [date.strftime('%Y-%m-%d')] end From 364832d552c1c1d30adc1ff9c9e72dd817f765d3 Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Fri, 12 Jun 2026 11:48:13 -0400 Subject: [PATCH 29/38] rubo 5 please --- app/indexers/base_resource_indexer.rb | 2 +- app/services/spot/workflow/activate_object.rb | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/indexers/base_resource_indexer.rb b/app/indexers/base_resource_indexer.rb index f7483c06f..2d4b40934 100644 --- a/app/indexers/base_resource_indexer.rb +++ b/app/indexers/base_resource_indexer.rb @@ -86,7 +86,7 @@ def parse_sortable_date # "Years Encompassed" meaning what years are covered by the metadata dates for a resource. # Handles individual dates and EDTF ranges (so "2001/2003" encompasses "2001", "2002", "2003"). # Used for the blacklight_range_limit plugin. - def parse_years_encompassed + def parse_years_encompassed fields = Array.wrap(years_encompassed_fields) return [] unless fields.any? { |field| resource.respond_to?(field) } diff --git a/app/services/spot/workflow/activate_object.rb b/app/services/spot/workflow/activate_object.rb index 891a6820d..8f72355fe 100644 --- a/app/services/spot/workflow/activate_object.rb +++ b/app/services/spot/workflow/activate_object.rb @@ -21,11 +21,12 @@ def self.call(target:, **kwargs) return true if target.respond_to?(:date_available) && target.date_available.present? if target.respond_to?(:date_available=) && target.date_available.blank? - date= if target.try(:embargo) && target.embargo.try(:embargo_release_date).present? - target.embargo.embargo_release_date - else - Time.zone.now - end + date = + if target.try(:embargo) && target.embargo.try(:embargo_release_date).present? + target.embargo.embargo_release_date + else + Time.zone.now + end target.date_available = [date.strftime('%Y-%m-%d')] end From 4795108d2d3818f0a065fabf078e8a6622af5cde Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Mon, 15 Jun 2026 09:52:20 -0400 Subject: [PATCH 30/38] git env update --- .github/workflows/lint-and-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 7d7482c2a..b67e6a8fd 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -79,6 +79,8 @@ jobs: SOLR_TEST_URL: http://solr:8983/solr/spot-test SOLR_VERSION: "9.10.1" URL_HOST: http://localhost:3000 + AWS_IIIF_ASSET_BUCKET: iiif-derivatives + AWS_AV_ASSET_BUCKET: av-derivatives steps: - uses: actions/checkout@v4 From 5342dea7eca17669b744638cdaa6ff997ac7b10a Mon Sep 17 00:00:00 2001 From: Jennifer Wellnitz Date: Mon, 15 Jun 2026 11:26:52 -0400 Subject: [PATCH 31/38] aws region --- .github/workflows/lint-and-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index b67e6a8fd..18020f06d 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -81,6 +81,7 @@ jobs: URL_HOST: http://localhost:3000 AWS_IIIF_ASSET_BUCKET: iiif-derivatives AWS_AV_ASSET_BUCKET: av-derivatives + AWS_REGION: us-east-1 steps: - uses: actions/checkout@v4 From 7820b75db8a4eebe0dbbd52412a3404930a8d4f0 Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Tue, 16 Jun 2026 16:58:10 -0400 Subject: [PATCH 32/38] images upload and render --- app/services/spot/iiif_service.rb | 25 +++++++++++++++++-------- app/services/spot/s3_path.rb | 10 ++++++++-- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/services/spot/iiif_service.rb b/app/services/spot/iiif_service.rb index c0488e906..2c94b8511 100644 --- a/app/services/spot/iiif_service.rb +++ b/app/services/spot/iiif_service.rb @@ -67,7 +67,7 @@ def initialize(file_id:, base_url: ENV['IIIF_BASE_URL']) # @note this produces a URL _without_ the final 'info.json' of the path. # Somewhere in the pipeline this is added (possibly by the viewer?) def info_url - URI.join(base_url, file_set_id).to_s + URI.join(base_url, asset_id).to_s end # Generates a IIIF image URL for an item @@ -80,13 +80,13 @@ def info_url # @option [String] format (default: 'jpg') # @return [String] def image_url(region: 'full', size: DEFAULT_SIZE, rotation: '0', quality: 'default', format: 'jpg') - URI.join(base_url, "#{file_set_id}/#{region}/#{size}/#{rotation}/#{quality}.#{format}").to_s + URI.join(base_url, "#{asset_id}/#{region}/#{size}/#{rotation}/#{quality}.#{format}").to_s end # Generates a IIIF image URL for an item that will trigger a download # # @param [Hash] options - # @option [String] filename (default: "#{file_set_id}.jpg") + # @option [String] filename (default: "#{asset_id}.jpg") # @option [String] region (default: 'full') # @option [String] size (default: DEFAULT_SIZE) # @option [String] rotation (default: '0') @@ -95,18 +95,27 @@ def image_url(region: 'full', size: DEFAULT_SIZE, rotation: '0', quality: 'defau # @return [String] # @see https://cantaloupe-project.github.io/manual/4.1/endpoints.html#Response%20Content%20Disposition def download_url(filename: nil, format: 'jpg', **args) - filename = "#{file_set_id}.#{format}" if filename.nil? + filename = "#{asset_id}.#{format}" if filename.nil? base_url = image_url(format: format, **args) "#{base_url}?response-content-disposition=attachment%3B%20#{filename}" end - # file_id will look like "abc123def/files/00000000-0000-0000-0000-000000000000", but all - # we really need is the first part (the id of the file_set) + private + + # file_ids are generally "/files/" although + # Valkyrized FileMetadata objects appear to have a fourth value after file id (version id?). + # With ActiveFedora, we tied the file to the FileSet id, but with Valkyrie + # we're using the FileMetadata id. # # @return [String] - def file_set_id - @file_set_id ||= CGI.unescape(file_id).split('/files/').first + def asset_id + @asset_id ||= + if Hyrax.config.use_valkyrie? + CGI.unescape(file_id).split('/')[2] + else + CGI.unescape(file_id).split('/files/').first + end end end end diff --git a/app/services/spot/s3_path.rb b/app/services/spot/s3_path.rb index 733787036..c27b10e4e 100644 --- a/app/services/spot/s3_path.rb +++ b/app/services/spot/s3_path.rb @@ -26,8 +26,14 @@ def initialize(base_path: nil); end # IIIF source images are created on disk before sending to S3, # so the desired filename has already been generated. class IiifPathGenerator < Base - def generate(original_filename:, **) - original_filename + def generate(resource:, file:, original_filename:) + puts "****** IIIFPATH GENERATOR ********" + puts "resource.id: #{resource.id}" + puts "file.id: #{file.inspect}" + puts "original_filename: #{original_filename}" + puts "****** /IIIFPATH GENERATOR *******" + + "#{resource.id}-access#{File.extname(original_filename)}" end end From 39222b94e3b41b169b3e4a58097b6f6f33ca0eb4 Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Tue, 16 Jun 2026 16:59:38 -0400 Subject: [PATCH 33/38] whoops / doc --- app/services/spot/iiif_service.rb | 6 ++---- app/services/spot/s3_path.rb | 8 +------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/app/services/spot/iiif_service.rb b/app/services/spot/iiif_service.rb index 2c94b8511..1afa7bdcd 100644 --- a/app/services/spot/iiif_service.rb +++ b/app/services/spot/iiif_service.rb @@ -103,12 +103,10 @@ def download_url(filename: nil, format: 'jpg', **args) private - # file_ids are generally "/files/" although - # Valkyrized FileMetadata objects appear to have a fourth value after file id (version id?). - # With ActiveFedora, we tied the file to the FileSet id, but with Valkyrie - # we're using the FileMetadata id. + # file_ids look like "/files/(/)" # # @return [String] + # @see https://github.com/samvera/hyrax/blob/hyrax-v5.2.0/app/models/hyrax/file_set.rb#L76-L83 def asset_id @asset_id ||= if Hyrax.config.use_valkyrie? diff --git a/app/services/spot/s3_path.rb b/app/services/spot/s3_path.rb index c27b10e4e..b3d62f678 100644 --- a/app/services/spot/s3_path.rb +++ b/app/services/spot/s3_path.rb @@ -26,13 +26,7 @@ def initialize(base_path: nil); end # IIIF source images are created on disk before sending to S3, # so the desired filename has already been generated. class IiifPathGenerator < Base - def generate(resource:, file:, original_filename:) - puts "****** IIIFPATH GENERATOR ********" - puts "resource.id: #{resource.id}" - puts "file.id: #{file.inspect}" - puts "original_filename: #{original_filename}" - puts "****** /IIIFPATH GENERATOR *******" - + def generate(resource:, file:, original_filename:) # rubocop:disable Lint/UnusedMethodArgument "#{resource.id}-access#{File.extname(original_filename)}" end end From 1e16d8deba964abfd1bd3e8703672e8090e7657c Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Tue, 16 Jun 2026 17:51:34 -0400 Subject: [PATCH 34/38] updated Gemfile.lock --- Gemfile.lock | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index abebe6435..f2e93339c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -382,6 +382,7 @@ GEM net-http (~> 0.5) faraday-retry (2.4.0) faraday (~> 2.0) + ffi (1.17.4) ffi (1.17.4-arm64-darwin) ffprober (2.0) flipflop (2.8.0) @@ -436,6 +437,9 @@ GEM faraday (>= 1.0, < 3.a) google-cloud-errors (1.6.0) google-logging-utils (0.2.0) + google-protobuf (4.35.1) + bigdecimal + rake (~> 13.3) google-protobuf (4.35.1-arm64-darwin) bigdecimal rake (~> 13.3) @@ -453,6 +457,9 @@ GEM os (>= 0.9, < 2.0) pstore (~> 0.1) signet (>= 0.16, < 2.a) + grpc (1.81.1) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) grpc (1.81.1-arm64-darwin) google-protobuf (>= 3.25, < 5.0) googleapis-common-protos-types (~> 1.0) @@ -713,6 +720,7 @@ GEM mime-types-data (3.2026.0414) mini_magick (4.13.2) mini_mime (1.1.5) + mini_portile2 (2.8.9) minitest (6.0.6) drb (~> 2.0) prism (~> 1.5) @@ -746,6 +754,9 @@ GEM noid-rails (3.3.0) actionpack (>= 5.0.0, < 9) noid (~> 0.9) + nokogiri (1.19.3) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) nokogiri (1.19.3-arm64-darwin) racc (~> 1.4) non-digest-assets (2.2.0) @@ -1207,6 +1218,7 @@ GEM PLATFORMS arm64-darwin-24 + ruby DEPENDENCIES almond-rails (~> 0.3.0) From f8629f793c0aefd8afa5fdefe380a79dc2b798ea Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Thu, 18 Jun 2026 17:59:17 -0400 Subject: [PATCH 35/38] move valkyrie config to its own initializer --- config/initializers/1_valkyrie.rb | 68 +++++++++++++++++++++++++++++++ config/initializers/wings.rb | 47 --------------------- 2 files changed, 68 insertions(+), 47 deletions(-) create mode 100644 config/initializers/1_valkyrie.rb diff --git a/config/initializers/1_valkyrie.rb b/config/initializers/1_valkyrie.rb new file mode 100644 index 000000000..8b565f200 --- /dev/null +++ b/config/initializers/1_valkyrie.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true +# +# Configuration for Valkyrie +Rails.application.config.after_initialize do + # We're using the "Freyja" metadata adapter, included with Hyrax, as a way to migrate off of our + # Fedora 4 instance and onto PostgreSQL (assets stored in S3): Freyja writes to Postgres and tries + # reading from Fedora before falling over to Postgres. This requires Hyrax's "Wings" adapter + # (specifically the ModelRegistry) to translate ActiveFedora models to Valkyrie ones. + # That configuration has its own file. + # + # @see config/initializers/wings.rb + Valkyrie::MetadataAdapter.register(Freyja::MetadataAdapter.new, :freyja) + Valkyrie.config.metadata_adapter = :freyja + + # We're still writing to Solr for search and browsing. The indexing adapter + # is registered in a Hyrax initializer. + # + # @see https://github.com/samvera/hyrax/blob/hyrax-v5.2.0/config/initializers/indexing_adapter_initializer.rb + Valkyrie.config.indexing_adapter = :solr_index + + # Set up Valkyrie storage adapters for the LDR's object store as well as + # IIIF and A/V derivatives. + # + # @see app/services/spot/derivatives/image_derivative_service.rb (:s3_iiif) + # @see https://github.com/samvera-labs/valkyrie-shrine/ + aws_opts = { force_path_style: !Rails.env.production? } + + Shrine.storages = { + s3_iiif: Valkyrie::Shrine::Storage::S3.new(bucket: ENV.fetch('AWS_IIIF_ASSET_BUCKET') { 'iiif-derivatives' }, **aws_opts), + s3_av: Valkyrie::Shrine::Storage::S3.new(bucket: ENV.fetch('AWS_AV_ASSET_BUCKET') { 'av-derivatives' }, **aws_opts), + s3_object_store: Valkyrie::Shrine::Storage::S3.new(bucket: ENV.fetch('AWS_OBJECT_STORE_BUCKET') { 'ldr-object-store' }, **aws_opts) + } + + # @note We need to use a custom PathGenerator for the valkyrie-shrine adapter, as the default one + # appends a uuid to the path to prevent overwrites, but as these are access derivatives, we're not + # particularly concerned about that. + # + # @see app/services/spot/s3_path.rb + Valkyrie::StorageAdapter.register( + Valkyrie::Storage::Shrine.new(Shrine.storages[:s3_iiif], nil, Spot::S3Path::IiifPathGenerator), + :iiif_source_s3 + ) + + Valkyrie::StorageAdapter.register( + Valkyrie::Storage::Shrine.new(Shrine.storages[:s3_av], nil, Spot::S3Path::AvPathGenerator), + :av_source_s3 + ) + + if ENV['AWS_OBJECT_STORE_BUCKET'].present? + Valkyrie::StorageAdapter.register( + Valkyrie::Storage::VersionedShrine.new(Shrine.storages[:s3_object_store]), + :versioned_object_store_s3 + ) + Valkyrie.config.storage_adapter = :versioned_object_store_s3 + Rails.logger.info("Storing object assets in s3://#{ENV['AWS_OBJECT_STORE_BUCKET']}") + else + base_path = Rails.root.join('storage', 'files') + Valkyrie::StorageAdapter.register( + Valkyrie::Storage::VersionedDisk.new( + base_path: base_path, + file_mover: FileUtils.method(:cp) + ), + :disk + ) + Valkyrie.config.storage_adapter = :disk + Rails.logger.info("Storing object assets on disk at #{base_path}") + end +end diff --git a/config/initializers/wings.rb b/config/initializers/wings.rb index 87e281002..42e6eee01 100644 --- a/config/initializers/wings.rb +++ b/config/initializers/wings.rb @@ -26,52 +26,6 @@ Wings::ModelRegistry.register(Hyrax::FileMetadata, Hydra::PCDM::File) Wings::ModelRegistry.register(Hydra::PCDM::File, Hydra::PCDM::File) - ## - # ADAPTERS SETUP - # metadata, indexing, storage - ## - - Valkyrie::MetadataAdapter.register(Freyja::MetadataAdapter.new, :freyja) - Valkyrie.config.metadata_adapter = :freyja - Hyrax.config.query_index_from_valkyrie = true - Hyrax.config.index_adapter = :solr_index - - Valkyrie::StorageAdapter.register( - Valkyrie::Storage::VersionedDisk.new( - base_path: Rails.root.join('storage', 'files'), - file_mover: FileUtils.method(:cp) - ), - :disk - ) - Valkyrie.config.storage_adapter = :disk - Valkyrie.config.indexing_adapter = :solr_index - - # Use valkyrie-shrine's s3 capabilities to store iiif source images as a way - # to use more Samvera-community code rather than rolling our own AWS client usage. - # - # @see app/services/spot/derivatives/image_derivative_service.rb (:s3_iiif) - # @see https://github.com/samvera-labs/valkyrie-shrine/ - aws_opts = { force_path_style: !Rails.env.production? } - - Shrine.storages = { - s3_iiif: Shrine::Storage::S3.new(bucket: ENV.fetch('AWS_IIIF_ASSET_BUCKET'), **aws_opts), - s3_av: Shrine::Storage::S3.new(bucket: ENV.fetch('AWS_AV_ASSET_BUCKET'), **aws_opts) - } - - # @note We need to use a custom PathGenerator for the valkyrie-shrine adapter, as the default one - # appends a uuid to the path to prevent overwrites, but as these are access derivatives, we're not - # particularly concerned about that. - # - # @see app/services/spot/s3_path.rb - Valkyrie::StorageAdapter.register( - Valkyrie::Storage::Shrine.new(Shrine.storages[:s3_iiif], nil, Spot::S3Path::IiifPathGenerator), - :iiif_source_s3 - ) - - Valkyrie::StorageAdapter.register( - Valkyrie::Storage::Shrine.new(Shrine.storages[:s3_av], nil, Spot::S3Path::AvPathGenerator), - :av_source_s3 - ) # The :solr_index adapter is set up in a Hyrax initializer, so we just need to ensure # that Hyrax and Valkyrie are configured to use it @@ -79,7 +33,6 @@ # @see https://github.com/samvera/hyrax/blob/hyrax-v5.2.0/config/initializers/indexing_adapter_initializer.rb Hyrax.config.query_index_from_valkyrie = true Hyrax.config.index_adapter = :solr_index - Valkyrie.config.indexing_adapter = :solr_index # load all the sql based custom queries [ From 136dc77a59cc7fddeb0b12783a0314e32d61df4a Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Thu, 18 Jun 2026 18:00:21 -0400 Subject: [PATCH 36/38] ensure object store bucket is created --- app/services/spot/iiif_service.rb | 6 +++++- bin/migrate-and-seed-db.sh | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/services/spot/iiif_service.rb b/app/services/spot/iiif_service.rb index 1afa7bdcd..7e053a3b6 100644 --- a/app/services/spot/iiif_service.rb +++ b/app/services/spot/iiif_service.rb @@ -103,10 +103,14 @@ def download_url(filename: nil, format: 'jpg', **args) private - # file_ids look like "/files/(/)" + # file_ids look like "/files/(/)". + # When we were on ActiveFedora, we used the file_set.id portion to determine the + # filename in S3. When using Valkyrie's storage adapters, the practice is to use + # the FileMetadata id (nee: file_id) to name the object. # # @return [String] # @see https://github.com/samvera/hyrax/blob/hyrax-v5.2.0/app/models/hyrax/file_set.rb#L76-L83 + # @see app/services/spot/s3_path.rb def asset_id @asset_id ||= if Hyrax.config.use_valkyrie? diff --git a/bin/migrate-and-seed-db.sh b/bin/migrate-and-seed-db.sh index d60264e07..579da7ad8 100755 --- a/bin/migrate-and-seed-db.sh +++ b/bin/migrate-and-seed-db.sh @@ -15,6 +15,11 @@ if [[ ! -z "$AWS_AV_ASSET_BUCKET" ]]; then aws --endpoint-url="${AWS_ENDPOINT_URL:-"http://localhost:9000"}" s3 mb "s3://${AWS_AV_ASSET_BUCKET}" fi +if [[ ! -z "$AWS_OBJECT_STORE_BUCKET" ]]; then + echo "creating object store bucket" + aws --endpoint-url="${AWS_ENDPOINT_URL:-"http://localhost:9000"}" s3 mb "s3://${AWS_OBJECT_STORE_BUCKET}" +fi + script_root="$(dirname $0)" $script_root/wait-for.sh db:5432 From 99bd677a9a4e81c7e5399e5b540b9eac2cf4fe3b Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Thu, 18 Jun 2026 18:01:18 -0400 Subject: [PATCH 37/38] we'll want to valkyrie off to add af objects for dev --- app/controllers/hyrax/audio_visuals_controller.rb | 3 +-- app/controllers/hyrax/images_controller.rb | 2 +- app/controllers/hyrax/publications_controller.rb | 3 +-- app/controllers/hyrax/student_works_controller.rb | 3 +-- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/controllers/hyrax/audio_visuals_controller.rb b/app/controllers/hyrax/audio_visuals_controller.rb index 9cacb18b9..ef44e2196 100644 --- a/app/controllers/hyrax/audio_visuals_controller.rb +++ b/app/controllers/hyrax/audio_visuals_controller.rb @@ -3,8 +3,7 @@ module Hyrax class AudioVisualsController < ApplicationController include ::Spot::WorksControllerBehavior - # self.curation_concern_type = ::AudioVisual - self.curation_concern_type = ::AudioVisualResource + self.curation_concern_type = Hyrax.config.use_valkyrie? ? AudioVisualResource : AudioVisual self.work_form_service = Hyrax::FormFactory.new self.show_presenter = Hyrax::AudioVisualPresenter diff --git a/app/controllers/hyrax/images_controller.rb b/app/controllers/hyrax/images_controller.rb index 391165a72..6728f16d1 100644 --- a/app/controllers/hyrax/images_controller.rb +++ b/app/controllers/hyrax/images_controller.rb @@ -4,7 +4,7 @@ class ImagesController < ApplicationController include ::Spot::WorksControllerBehavior # self.curation_concern_type = ::Image - self.curation_concern_type = ::ImageResource + self.curation_concern_type = Hyrax.config.use_valkyrie? ? ImageResource : Image self.work_form_service = Hyrax::FormFactory.new self.show_presenter = Hyrax::ImagePresenter diff --git a/app/controllers/hyrax/publications_controller.rb b/app/controllers/hyrax/publications_controller.rb index f195b7c8c..f35136a75 100644 --- a/app/controllers/hyrax/publications_controller.rb +++ b/app/controllers/hyrax/publications_controller.rb @@ -3,8 +3,7 @@ module Hyrax class PublicationsController < ApplicationController include Spot::WorksControllerBehavior - # self.curation_concern_type = ::Publication - self.curation_concern_type = ::PublicationResource + self.curation_concern_type = Hyrax.config.use_valkyrie? ? PublicationResource : Publication self.work_form_service = Hyrax::FormFactory.new self.show_presenter = Hyrax::PublicationPresenter diff --git a/app/controllers/hyrax/student_works_controller.rb b/app/controllers/hyrax/student_works_controller.rb index 0c3c7d32e..cd0a51a1c 100644 --- a/app/controllers/hyrax/student_works_controller.rb +++ b/app/controllers/hyrax/student_works_controller.rb @@ -3,8 +3,7 @@ module Hyrax class StudentWorksController < ApplicationController include Spot::WorksControllerBehavior - # self.curation_concern_type = ::StudentWork - self.curation_concern_type = ::StudentWorkResource + self.curation_concern_type = Hyrax.config.use_valkyrie? ? StudentWorkResource : StudentWork self.work_form_service = Hyrax::FormFactory.new self.show_presenter = Hyrax::StudentWorkPresenter From 5f43f59a03136da39af55b34ff6488e798885548 Mon Sep 17 00:00:00 2001 From: Anna Malantonio Date: Mon, 22 Jun 2026 14:09:22 -0400 Subject: [PATCH 38/38] ensure the shrine storages have different prefixes --- config/initializers/1_valkyrie.rb | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/config/initializers/1_valkyrie.rb b/config/initializers/1_valkyrie.rb index 8b565f200..2c38e76a7 100644 --- a/config/initializers/1_valkyrie.rb +++ b/config/initializers/1_valkyrie.rb @@ -23,32 +23,40 @@ # # @see app/services/spot/derivatives/image_derivative_service.rb (:s3_iiif) # @see https://github.com/samvera-labs/valkyrie-shrine/ + # @note minio in development requires use to use path style s3 urls rather than hostnamed aws_opts = { force_path_style: !Rails.env.production? } Shrine.storages = { + s3_object_store: Valkyrie::Shrine::Storage::S3.new(bucket: ENV.fetch('AWS_OBJECT_STORE_BUCKET') { 'ldr-object-store' }, **aws_opts), s3_iiif: Valkyrie::Shrine::Storage::S3.new(bucket: ENV.fetch('AWS_IIIF_ASSET_BUCKET') { 'iiif-derivatives' }, **aws_opts), - s3_av: Valkyrie::Shrine::Storage::S3.new(bucket: ENV.fetch('AWS_AV_ASSET_BUCKET') { 'av-derivatives' }, **aws_opts), - s3_object_store: Valkyrie::Shrine::Storage::S3.new(bucket: ENV.fetch('AWS_OBJECT_STORE_BUCKET') { 'ldr-object-store' }, **aws_opts) + s3_av: Valkyrie::Shrine::Storage::S3.new(bucket: ENV.fetch('AWS_AV_ASSET_BUCKET') { 'av-derivatives' }, **aws_opts) } + # As we're using multiple buckets for different purposes (IIIF derivatives vs AV derivatives vs Object Store) + # we'll want to utilize the `identifier_prefix` to store the intended bucket as part of the file's remote uri. + # + # @example prefixed remote uri + # + # + # # @note We need to use a custom PathGenerator for the valkyrie-shrine adapter, as the default one # appends a uuid to the path to prevent overwrites, but as these are access derivatives, we're not # particularly concerned about that. # # @see app/services/spot/s3_path.rb Valkyrie::StorageAdapter.register( - Valkyrie::Storage::Shrine.new(Shrine.storages[:s3_iiif], nil, Spot::S3Path::IiifPathGenerator), + Valkyrie::Storage::Shrine.new(Shrine.storages[:s3_iiif], nil, Spot::S3Path::IiifPathGenerator, identifier_prefix: 'iiif'), :iiif_source_s3 ) Valkyrie::StorageAdapter.register( - Valkyrie::Storage::Shrine.new(Shrine.storages[:s3_av], nil, Spot::S3Path::AvPathGenerator), + Valkyrie::Storage::Shrine.new(Shrine.storages[:s3_av], nil, Spot::S3Path::AvPathGenerator, identifier_prefix: 'av'), :av_source_s3 ) if ENV['AWS_OBJECT_STORE_BUCKET'].present? Valkyrie::StorageAdapter.register( - Valkyrie::Storage::VersionedShrine.new(Shrine.storages[:s3_object_store]), + Valkyrie::Storage::VersionedShrine.new(Shrine.storages[:s3_object_store], identifier_prefix: 'obj'), :versioned_object_store_s3 ) Valkyrie.config.storage_adapter = :versioned_object_store_s3