diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 7d7482c2a..18020f06d 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -79,6 +79,9 @@ 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 + AWS_REGION: us-east-1 steps: - uses: actions/checkout@v4 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/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/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..f2e93339c 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,32 @@ 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) + ffi (1.17.4-arm64-darwin) + ffprober (2.0) flipflop (2.8.0) activesupport (>= 4.0) terminal-table (>= 1.8) @@ -387,10 +392,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 +412,77 @@ 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) + bigdecimal + rake (~> 13.3) + 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) google-protobuf (>= 3.25, < 5.0) googleapis-common-protos-types (~> 1.0) - haml (6.3.0) + grpc (1.81.1-arm64-darwin) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.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 +493,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 +521,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 +601,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 +611,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 +632,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 +661,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 +699,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 +711,26 @@ 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 +738,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,9 +754,11 @@ GEM noid-rails (3.3.0) actionpack (>= 5.0.0, < 9) noid (~> 0.9) - nokogiri (1.19.0) + 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) activesupport (>= 5.2, < 7.1) sprockets (>= 2.0, < 5.0) @@ -749,12 +767,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 +788,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 +852,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 +866,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 +877,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 +963,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 +975,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 +993,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 +1004,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 +1016,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 +1051,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 +1084,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 +1095,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 +1113,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 +1147,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 +1156,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 +1175,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 +1185,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,20 +1203,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 + arm64-darwin-24 ruby DEPENDENCIES @@ -1241,6 +1292,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/controllers/hyrax/audio_visuals_controller.rb b/app/controllers/hyrax/audio_visuals_controller.rb index 94f74f22c..ef44e2196 100644 --- a/app/controllers/hyrax/audio_visuals_controller.rb +++ b/app/controllers/hyrax/audio_visuals_controller.rb @@ -3,9 +3,9 @@ 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 = Hyrax.config.use_valkyrie? ? AudioVisualResource : AudioVisual + 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..6728f16d1 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 = Hyrax.config.use_valkyrie? ? ImageResource : Image + 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..f35136a75 100644 --- a/app/controllers/hyrax/publications_controller.rb +++ b/app/controllers/hyrax/publications_controller.rb @@ -3,7 +3,9 @@ module Hyrax class PublicationsController < ApplicationController include Spot::WorksControllerBehavior - self.curation_concern_type = ::Publication + self.curation_concern_type = Hyrax.config.use_valkyrie? ? PublicationResource : Publication + 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..cd0a51a1c 100644 --- a/app/controllers/hyrax/student_works_controller.rb +++ b/app/controllers/hyrax/student_works_controller.rb @@ -3,7 +3,9 @@ module Hyrax class StudentWorksController < ApplicationController include Spot::WorksControllerBehavior - self.curation_concern_type = ::StudentWork + self.curation_concern_type = Hyrax.config.use_valkyrie? ? StudentWorkResource : StudentWork + 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/audio_visual_resource_form.rb b/app/forms/audio_visual_resource_form.rb new file mode 100644 index 000000000..a3f6e4443 --- /dev/null +++ b/app/forms/audio_visual_resource_form.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +class AudioVisualResourceForm < Hyrax::Forms::ResourceForm(AudioVisualResource) + include Spot::Forms::BaseResourceFormBehavior + + include Hyrax::FormFields(:audio_visual_metadata) + + language_tagged_field(:inscription) + + def primary_terms # rubocop:disable Metrics/MethodLength + [ + # required_fields first + :title, + :date, + :resource_type, + :rights_statement, + + # non-required fields + :rights_holder, + :subtitle, + :title_alternative, + :date_associated, + :creator, + :contributor, + :publisher, + :source, + :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/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..a677dff06 --- /dev/null +++ b/app/forms/concerns/spot/forms/base_resource_form_behavior.rb @@ -0,0 +1,76 @@ +# 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) + + 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..ba069a099 --- /dev/null +++ b/app/forms/concerns/spot/forms/language_tagged_fields.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true +module Spot + 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 + # 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) + deserialize_rdf(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| + deserialize_rdf(value)&.language + end + + send(:"#{field}_language=", languages) + end + 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..f111e069d --- /dev/null +++ b/app/forms/concerns/spot/forms/nested_attributes.rb @@ -0,0 +1,78 @@ +# 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 is an ActionController::Parameters object + fragment.to_unsafe_hash.each do |_idx, attrs| + value = attrs[value_key] + if attrs[destroy_key] == 'true' + deletes << value + else + adds << value + end + end + + original_values = Array.wrap(model.send(field)).map(&:to_s) + merged_values = ((original_values + 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/forms/image_resource_form.rb b/app/forms/image_resource_form.rb new file mode 100644 index 000000000..c72648aee --- /dev/null +++ b/app/forms/image_resource_form.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true +class ImageResourceForm < Hyrax::Forms::ResourceForm(ImageResource) + include Spot::Forms::BaseResourceFormBehavior + + include Hyrax::FormFields(:image_metadata) + + language_tagged_field(:inscription) + + def primary_terms # rubocop:disable Metrics/MethodLength + [ + :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 new file mode 100644 index 000000000..91fbc0ebd --- /dev/null +++ b/app/forms/publication_resource_form.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +class PublicationResourceForm < Hyrax::Forms::ResourceForm(PublicationResource) + include Spot::Forms::BaseResourceFormBehavior + + include Hyrax::FormFields(:publication_metadata) + include Hyrax::FormFields(:institutional_metadata) + + 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/forms/student_work_resource_form.rb b/app/forms/student_work_resource_form.rb new file mode 100644 index 000000000..49d1f9e29 --- /dev/null +++ b/app/forms/student_work_resource_form.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +class StudentWorkResourceForm < Hyrax::Forms::ResourceForm(StudentWorkResource) + include Spot::Forms::BaseResourceFormBehavior + + include Hyrax::FormFields(:student_work_metadata) + include Hyrax::FormFields(:institutional_metadata) + + nested_attributes_for(:academic_department, :division, :advisor) + + 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 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..2d4b40934 --- /dev/null +++ b/app/indexers/base_resource_indexer.rb @@ -0,0 +1,122 @@ +# 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 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 + } + ) + + # 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 + + private + + # rubocop:disable Metrics/CyclomaticComplexity + def citation_metadata + return {} unless resource.respond_to?(:bibliographic_citation) && resource.bibliographic_citation.present? + + 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.created_at.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) } + + # rubocop:disable Style/BlockDelimiters + 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..a962e0959 --- /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 diff --git a/app/indexers/student_work_resource_indexer.rb b/app/indexers/student_work_resource_indexer.rb new file mode 100644 index 000000000..fca90e818 --- /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'] = (resource.try(:advisor) || []).to_a + solr_doc['advisor_label_ssim'] = (resource.try(: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/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) 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/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..0a16a540f --- /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 + + 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 3cf3507f3..735c428e5 100644 --- a/app/models/audio_visual_resource.rb +++ b/app/models/audio_visual_resource.rb @@ -1,7 +1,18 @@ # 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) + 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 + attribute :stored_derivatives, Valkyrie::Types::String end 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/collection_resource.rb b/app/models/collection_resource.rb new file mode 100644 index 000000000..7aaf5592b --- /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) + + attribute :internal_resource, Valkyrie::Types::Any.default('Collection'), internal: true +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..e5b373d84 --- /dev/null +++ b/app/models/concerns/spot/has_controlled_fields.rb @@ -0,0 +1,22 @@ +# 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 + # rubocop:disable Naming/PredicateName + 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 9de64fee0..b8b6687f0 100644 --- a/app/models/image_resource.rb +++ b/app/models/image_resource.rb @@ -1,5 +1,16 @@ # 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) + + 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/publication_resource.rb b/app/models/publication_resource.rb index 5900482b7..e67270e55 100644 --- a/app/models/publication_resource.rb +++ b/app/models/publication_resource.rb @@ -1,6 +1,17 @@ # frozen_string_literal: true -class PublicationResource < ::Hyrax::Work - include Hyrax::Schema(:base_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) + + 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/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/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 316197ce9..72464bd74 100644 --- a/app/models/student_work_resource.rb +++ b/app/models/student_work_resource.rb @@ -1,6 +1,17 @@ # 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) + + 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/user.rb b/app/models/user.rb index f21e18629..632cbcc00 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -20,6 +20,16 @@ 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..7787c353a --- /dev/null +++ b/app/presenters/concerns/spot/lucene_patch_for_pcdm_member_presenters_factory.rb @@ -0,0 +1,22 @@ +# 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(',')}" + 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') + .fetch('response') + .fetch('docs') + end + end +end 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 diff --git a/app/services/spot/derivatives/base_derivative_service.rb b/app/services/spot/derivatives/base_derivative_service.rb index 0494520a9..a5fcac4c9 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..e19b33bba --- /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 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/iiif_service.rb b/app/services/spot/iiif_service.rb index c0488e906..7e053a3b6 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,29 @@ 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 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] - def file_set_id - @file_set_id ||= CGI.unescape(file_id).split('/files/').first + # @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? + 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 new file mode 100644 index 000000000..b3d62f678 --- /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(resource:, file:, original_filename:) # rubocop:disable Lint/UnusedMethodArgument + "#{resource.id}-access#{File.extname(original_filename)}" + end + end + + # @todo get this from Jenn's work + class AvPathGenerator < Base + def generate(resource:, file:, original_filename:); end + end + end +end diff --git a/app/services/spot/workflow/activate_object.rb b/app/services/spot/workflow/activate_object.rb index 8784f9dc9..8f72355fe 100644 --- a/app/services/spot/workflow/activate_object.rb +++ b/app/services/spot/workflow/activate_object.rb @@ -18,15 +18,19 @@ 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 + 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 - # 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/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/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 diff --git a/config/application.rb b/config/application.rb index 9a64b2196..392858c31 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,27 @@ 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/1_valkyrie.rb b/config/initializers/1_valkyrie.rb new file mode 100644 index 000000000..2c38e76a7 --- /dev/null +++ b/config/initializers/1_valkyrie.rb @@ -0,0 +1,76 @@ +# 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/ + # @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) + } + + # 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, identifier_prefix: 'iiif'), + :iiif_source_s3 + ) + + Valkyrie::StorageAdapter.register( + 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], identifier_prefix: 'obj'), + :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/hyrax.rb b/config/initializers/hyrax.rb index 1ec742121..43e034253 100644 --- a/config/initializers/hyrax.rb +++ b/config/initializers/hyrax.rb @@ -2,17 +2,32 @@ 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 + # 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 @@ -95,7 +110,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' } @@ -314,15 +329,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/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/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) diff --git a/config/initializers/spot_overrides.rb b/config/initializers/spot_overrides.rb index 6e135c407..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' } @@ -168,8 +161,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 @@ -356,26 +347,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 # @@ -424,15 +418,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 diff --git a/config/initializers/wings.rb b/config/initializers/wings.rb index b47bea4bc..42e6eee01 100644 --- a/config/initializers/wings.rb +++ b/config/initializers/wings.rb @@ -1,19 +1,85 @@ # 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 -# @see https://github.com/samvera/hyrax/blob/hyrax-v5.2.0/.dassie/config/initializers/wings.rb Rails.application.config.after_initialize do - Wings::ModelRegistry.register(Collection, Collection) + # active_fedora models we're migrating + [Publication, Image, StudentWork, AudioVisual].each do |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) + end + + # 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(Hydra::PCDM::File, Hydra::PCDM::File) + Wings::ModelRegistry.register(Hyrax::FileMetadata, Hydra::PCDM::File) + Wings::ModelRegistry.register(Hydra::PCDM::File, Hydra::PCDM::File) + + + # 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 + + # 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 + # 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$/, '') + + next "#{klass_name}Resource".constantize if resource_types.include?(klass_name) - Wings::ModelRegistry.register(Publication, PublicationResource) - Wings::ModelRegistry.register(Image, ImageResource) - Wings::ModelRegistry.register(StudentWork, StudentWorkResource) - Wings::ModelRegistry.register(AudioVisual, AudioVisualResource) + case klass_name + when 'Hydra::AccessControl' + # Without this mapping, we'll see cases of Postgres Valkyrie adapter attempting to write to + # Fedora. Yeah! + Hyrax::AccessControl + when 'FileSet' + Hyrax::FileSet + when 'Hydra::AccessControls::Embargo' + Hyrax::Embargo + when 'Hydra::AccessControls::Lease' + Hyrax::Lease + when 'Hydra::PCDM::File' + 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..7fe7adfec 100644 --- a/config/metadata/audio_visual_metadata.yaml +++ b/config/metadata/audio_visual_metadata.yaml @@ -1,4 +1,13 @@ attributes: + barcode: + predicate: https://schema.org/Barcode + type: string + multiple: true + form: + multiple: true + primary: true + index_keys: + - barcode_ssim date: predicate: http://purl.org/dc/terms/date type: string @@ -6,6 +15,7 @@ attributes: required: true form: multiple: true + primary: true index_keys: - date_ssim date_associated: @@ -14,6 +24,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - date_associated_ssim - date_associated_tesim @@ -23,6 +34,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - inscription_tesim original_item_extent: @@ -31,6 +43,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - original_item_extent_tesim repository_location: @@ -39,6 +52,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - repository_location_ssim research_assistance: @@ -47,6 +61,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - research_assistance_ssim provenance: @@ -55,13 +70,6 @@ attributes: multiple: true form: multiple: true + primary: 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/config/metadata/base_metadata.yaml b/config/metadata/base_metadata.yaml index 7e86872ab..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,16 +52,17 @@ attributes: multiple: true form: multiple: true + primary: true 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 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: @@ -66,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: @@ -80,6 +87,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - physical_medium_tesim - physical_medium_sim @@ -89,6 +97,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - publisher_tesim - publisher_sim @@ -98,6 +107,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - related_resource_tesim - related_resource_sim @@ -108,6 +118,7 @@ attributes: form: multiple: true required: true + primary: true index_keys: - resource_type_ssim rights_holder: @@ -116,6 +127,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - rights_holder_tesim - rights_holder_sim @@ -126,6 +138,7 @@ attributes: form: multiple: false required: true + primary: true index_keys: - rights_statement_ssim source: @@ -134,6 +147,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - source_tesim - source_sim @@ -159,6 +173,7 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - subtitle_tesim - subtitle_sim @@ -168,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/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 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 39c697708..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,8 +23,10 @@ attributes: multiple: true form: multiple: true + primary: 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 @@ -31,6 +35,7 @@ attributes: required: true form: multiple: true + primary: true index_keys: - date_ssim date_available: @@ -39,5 +44,6 @@ attributes: multiple: true form: multiple: true + primary: true index_keys: - date_available_ssim 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/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 84033ac74..cf7173ccf 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1,7 +1,7 @@ -\restrict zGnP9xNrj5vXJP6eZcBchqhC4KoF4Tbn1lTESweMCy6CGbvkCQR9hwyeVf05jRP +\restrict lx2vxHyypdaSFHgcml7gnYOwJSn6kWDgeI4fkIiDH4YbMpXFTHeLTbbUiVFAd06 --- 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 lx2vxHyypdaSFHgcml7gnYOwJSn6kWDgeI4fkIiDH4YbMpXFTHeLTbbUiVFAd06 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'); 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 new file mode 100644 index 000000000..6159cca17 --- /dev/null +++ b/spec/indexers/publication_resource_indexer_spec.rb @@ -0,0 +1,141 @@ +# 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(: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 + + # 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 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..820cb8d21 --- /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 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..516b7731c --- /dev/null +++ b/spec/models/image_resource_spec.rb @@ -0,0 +1,71 @@ +# 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 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