diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 8f821ca0d..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: 2.1 - -jobs: - echo_skip: - docker: - - image: cimg/base:current - steps: - - run: - command: echo "skipping circleci tests" - -workflows: - version: 2 - noop: - jobs: - - echo_skip diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index f1ec003ae..4afe43098 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -19,10 +19,16 @@ jobs: run: bundle exec rubocop --format github test: - name: Test + name: "Test (Valkyrie enabled: ${{ github.matrix.hyrax_valkyrie_enabled }})" runs-on: ubuntu-latest - container: ruby:2.7.8-slim-bullseye + container: + image: ruby:3.2.9-slim-bookworm + volumes: + - solr:/tmp/solr needs: lint + strategy: + matrix: + hyrax_valkyrie_enabled: [false] services: database: image: postgres:13-alpine @@ -47,33 +53,19 @@ jobs: image: redis:6.2-alpine ports: - 6379:6379 - zookeeper: - image: bitnami/zookeeper:3.9 - ports: - - 2181:2181 - env: - ZOO_SERVER_ID: 1 - ALLOW_ANONYMOUS_LOGIN: yes - ZOO_SERVERS: zookeeper:2888:3888 - ZOO_4LW_COMMANDS_WHITELIST: srvr,mntr,conf,ruok solr: - image: bitnami/solr:8.11.2 + image: solr:8.11.4 ports: - 8983:8983 - env: - SOLR_ADMIN_USERNAME: solr_admin - SOLR_ADMIN_PASSWORD: solr_password - SOLR_CLOUD_BOOTSTRAP: yes - SOLR_ENABLE_AUTHENTICATION: yes - SOLR_ENABLE_CLOUD_MODE: yes - SOLR_HOST: solr - SOLR_ZK_HOSTS: zookeeper:2181 + volumes: + - solr:/var/solr/data env: BUNDLE_PATH: vendor/bundle BUNDLE_WITH: test CAS_BASE_URL: '' CI: 1 FEDORA_TEST_URL: http://fedora:8080/rest + HYRAX_VALKYRIE: ${{ matrix.hyrax_valkyrie_enabled }} IIIF_BASE_URL: http://localhost/iiif/2 NOKOGIRI_USE_SYSTEM_LIBRARIES: true PSQL_PASSWORD: spot_test_pw @@ -81,21 +73,31 @@ jobs: PSQL_DATABASE: spot_test PSQL_HOST: database RAILS_ENV: test - SOLR_TEST_URL: http://solr_admin:solr_password@solr:8983/solr/spot-test + SOLR_TEST_URL: http://solr:8983/solr/spot-test + SOLR_VERSION: "8.11.4" URL_HOST: http://localhost:3000 steps: + - uses: actions/checkout@v4 + - name: Copy Solr config to mount + run: | + mkdir -p /tmp/solr/spot-test/data + cp -r docker/solr/config/* /tmp/solr/spot-test + touch /tmp/solr/spot-test/core.properties + chown -R 8983:8983 /tmp/solr/spot-test + - name: Restart Solr service + uses: docker://docker + with: + args: docker restart ${{ job.services.solr.id }} - name: Install system dependencies from Dockerfile run: | - apt-get update -y && apt-get install -y --no-install-recommends ca-certificates curl gnupg && \ - mkdir -p /etc/apt/keyrings && \ - curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ - echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \ - apt-get update -y && apt-get install -y \ - build-essential coreutils curl git libxml2 libxml2-dev libxslt-dev \ - netcat-openbsd nodejs openssl postgresql-13 ruby-dev tzdata zip \ - libasound2 libdbus-glib-1-dev libgtk-3-0 libpq-dev unzip libx11-xcb1 + apt-get update -y && apt-get install -y curl && \ + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get update -y --fix-missing && apt-get install -y --no-install-recommends \ + awscli build-essential coreutils cron git libjemalloc2 libpq-dev libxml2 \ + libxml2-dev libxslt-dev netcat-openbsd nodejs openssl postgresql ruby-dev \ + tzdata zip libasound2 libdbus-glib-1-dev libgtk-3-0 libpq-dev unzip libx11-xcb1 - name: Setup Firefox uses: browser-actions/setup-firefox@v1 @@ -113,27 +115,6 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} geckodriver-version: latest - - - name: Checkout code - uses: actions/checkout@v4 - - - name: Restore Solr config.zip from cache - id: solr-config-cache - uses: actions/cache@v4 - with: - path: /tmp/spot_solr_config.zip - key: spot-solr-${{ hashFiles('docker/solr/config') }} - - - name: Build Solr config.zip - if: steps.solr-config-cache.outputs.cache-hit != 'true' - run: | - cd docker/solr/config - zip -1 /tmp/spot_solr_config.zip ./* - - - name: Setup Solr core with local configs - run: | - curl --user solr_admin:solr_password -H "Content-type: application/octet-stream" --data-binary @/tmp/spot_solr_config.zip "http://solr:8983/solr/admin/configs?action=UPLOAD&name=spot" - curl --user solr_admin:solr_password -H "Content-type: application/json" http://solr:8983/api/collections/ -d "{create: {name: spot-test, config: spot, numShards: 1}}" - name: Restore installed Gems uses: actions/cache@v4 @@ -145,6 +126,14 @@ jobs: run: | gem install bundler:$(tail -n 1 Gemfile.lock | sed -e "s/ *//") bundle install + - + name: Status check Solr + uses: cygnetdigital/wait_for_response@v2.0.0 + with: + url: http://solr:8983/solr/ + responseCode: 200,301 + timeout: 10000 + interval: 1000 - name: Run migrations run: bundle exec rake db:migrate diff --git a/.rspec b/.rspec index c99d2e739..7d5b1a7e2 100644 --- a/.rspec +++ b/.rspec @@ -1 +1 @@ ---require spec_helper +--require ./spec/spec_helper.rb --default-path spec diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c5c4d8114..a376959b8 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2024-09-04 19:26:55 UTC using RuboCop version 1.28.2. +# on 2025-05-12 16:44:26 UTC using RuboCop version 1.28.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -13,12 +13,11 @@ Layout/FirstArrayElementIndentation: EnforcedStyle: consistent -# Offense count: 1 -# This cop supports safe auto-correction (--auto-correct). -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns. -# URISchemes: http, https -Layout/LineLength: - Max: 215 +# Offense count: 2 +Lint/MissingSuper: + Exclude: + - 'app/forms/concerns/spot/forms/controlled_vocabulary_form_field.rb' + - 'app/forms/concerns/spot/forms/language_tagged_form_fields.rb' # Offense count: 7 # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods, inherit_mode. @@ -26,10 +25,10 @@ Layout/LineLength: Metrics/BlockLength: Max: 58 -# Offense count: 5 +# Offense count: 7 # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. Metrics/MethodLength: - Max: 27 + Max: 29 # Offense count: 1 # This cop supports safe auto-correction (--auto-correct). diff --git a/.ruby-version b/.ruby-version index 6a81b4c83..e650c01d9 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.8 +3.2.9 diff --git a/Dockerfile b/Dockerfile index 4d1b5d3e4..230fb9110 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,19 +3,19 @@ # !! This is a builder image. Not for general use !! # Use this as the base image for the Rails / Sidekiq services. ## -FROM ruby:2.7.8-slim-bullseye AS spot-base +FROM ruby:3.2.9-slim-bookworm AS spot-base RUN apt-get clean && \ - apt-get update && \ - apt-get install -y --no-install-recommends ca-certificates curl gnupg && \ - mkdir -p /etc/apt/keyrings && \ - curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ - echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \ - apt-get update -y && apt-get install -y --no-install-recommends \ + apt-get update -y && \ + apt-get install -y curl && \ + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get update -y --fix-missing && apt-get install -y --no-install-recommends \ awscli \ build-essential \ coreutils \ + cron \ git \ + libjemalloc2 \ libpq-dev \ libxml2 \ libxml2-dev \ @@ -23,7 +23,7 @@ RUN apt-get clean && \ netcat-openbsd \ nodejs \ openssl \ - postgresql-13 \ + postgresql \ ruby-dev \ tzdata \ zip @@ -35,12 +35,17 @@ ENV HYRAX_CACHE_PATH=/spot/tmp/cache \ HYRAX_UPLOAD_PATH=/spot/tmp/uploads \ BUNDLE_FORCE_RUBY_PLATFORM=1 +# configure ruby to use jemalloc + yjit to improve performance +# @see https://matthaliski.com/blog/upgrading-to-rails-7-1-ruby-3-3-and-jemalloc +ENV LD_PRELOAD="libjemalloc.so.2" \ + MALLOC_CONFIG="dirty_decay_ms:1000,narenas:2,background_thread:true,stats_print:true" \ + RUBY_YJIT_ENABLE="1" + RUN corepack enable -COPY Gemfile.lock /spot/ +COPY Gemfile Gemfile.lock /spot/ RUN gem install bundler:$(tail -n 1 Gemfile.lock | sed -e 's/\s*//') -COPY Gemfile /spot/ RUN bundle config unset with && \ bundle config unset without && \ bundle config set without "development:test" && \ @@ -50,7 +55,7 @@ ARG build_date="" ENV SPOT_BUILD_DATE="$build_date" ENTRYPOINT ["/spot/bin/spot-entrypoint.sh"] -CMD ["bundle", "exec", "rails", "server", "-b", "ssl://0.0.0.0:443?key=/spot/tmp/ssl/application.key&cert=/spot/tmp/ssl/application.crt"] +CMD ["bundle", "exec", "rails", "server", "-b", "ssl://0.0.0.0:443?key=/spot/tmp/ssl/application.key&cert=/spot/tmp/ssl/application.crt&verify_mode=peer"] HEALTHCHECK CMD curl -skf https://localhost/healthcheck/default || exit 1 @@ -132,18 +137,18 @@ RUN unzip -d /tmp/fits /tmp/fits.zip && \ ## FROM spot-base AS spot-worker-base # @see https://github.com/mperham/sidekiq/wiki/Memory#bloat -ENV MALLOC_ARENA_MAX=2 +# ENV MALLOC_ARENA_MAX=2 # We don't need the entrypoint script to generate an SSL cert ENV SKIP_SSL_CERT=true -RUN apt-get update && apt-get install -y --no-install-recommends \ - bash \ +RUN apt-get update -y && \ + apt-get install -y \ ffmpeg \ ghostscript \ imagemagick \ libreoffice \ mediainfo \ - openjdk-11-jre \ + openjdk-17-jre \ perl \ python3 \ unzip diff --git a/Gemfile b/Gemfile index db585bb3a..a75e80be6 100644 --- a/Gemfile +++ b/Gemfile @@ -9,13 +9,13 @@ end # # the base rails stack (installed with 'rails new spot') # -gem 'rails', '~> 5.2.7' +gem 'rails', '~> 6.1' # use Puma as the app server gem 'puma', '~> 6.4.0' # Use SCSS for stylesheets -gem 'sass-rails', '~> 5.1.0' +gem 'sass-rails', '~> 6.0' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '~> 4.2.0' @@ -29,7 +29,7 @@ gem 'jbuilder', '~> 2.11.5' # # the hyrax/spot stack # -gem 'hyrax', '~> 3.6.0' +gem 'hyrax', '~> 5.2.0' # modularize our javascripts gem 'almond-rails', '~> 0.3.0' @@ -44,15 +44,17 @@ gem 'aws-sdk-s3', '~> 1.142.0' gem 'bagit', '~> 0.6.0' # blacklight plugins for enhanced searching -gem 'blacklight_advanced_search', '~> 6.4.1' -gem 'blacklight_oai_provider', '~> 6.0.0' -gem 'blacklight_range_limit', '~> 6.3.3' +gem 'blacklight_advanced_search', '~> 7.0.0' +gem 'blacklight_oai_provider', '~> 7.0.2' +gem 'blacklight_range_limit', '~> 8.5.0' # start up the server faster gem 'bootsnap', '~> 1.17', require: false +gem 'bootstrap', '~> 4.0' + # Bulkrax for batch ingesting objects -gem 'browse-everything', '~> 1.1.2' +gem 'browse-everything', '~> 1.6.0' gem 'bulkrax', '~> 9.0.2' # This needs to be here if we want to compile our own JS @@ -73,10 +75,10 @@ gem 'edtf-humanize', '~> 2.1.0' # a bunch of samvera gems rely on Faraday already, but we'll # require it as we're explicitly using it. -gem 'faraday', '~> 0.17.6' +gem 'faraday', '~> 2.0' # video file resource for getting information on video derivatives -gem 'ffprober' +gem 'ffprober', '~> 1.0' # error trackijng gem 'honeybadger', '~> 4.12.1' @@ -99,7 +101,7 @@ gem 'kaminari', '~> 1.2.2' # mini_magick is a dependency of hydra-derivatives, but since we're # calling it explicitly, we should require it. -gem 'mini_magick', '~> 4.11' +gem 'mini_magick', '~> 4.13.2' # manually add this gem to enable questioning_authority to parse linked-data results gem 'linkeddata', '~> 3.1.6' @@ -116,7 +118,7 @@ gem 'pg', '~> 1.5.4' # this is bundled somewhere within the hyrax stack, but since we're # calling it within our code we shouldn't expect it to always be there -gem 'rdf-vocab', '~> 3.2.7' +gem 'rdf-vocab', '~> 3.3.3' # solr client for interacting with rails (installed w/ hyrax) gem 'rsolr', '~> 2.5.0' @@ -129,7 +131,15 @@ gem 'sidekiq', '~> 5.2.9' gem 'sidekiq-cron', '~> 1.9.1' # using Slack for some of our messaging -gem 'slack-ruby-client', '~> 0.14.6' +gem 'slack-ruby-client' + +# used in the Hyrax 4 upgrade but not a dependency?? +gem 'twitter-typeahead-rails', '~> 0.11.1' + +# pin sprockets to 3.7.2 to prevent javascript compilation errors from hyrax source +# @see https://github.com/samvera/hyrax/issues/6826 +gem 'sprockets', '3.7.2' +gem 'sprockets-rails', '3.4.2' # now that we're writing es6 javascript of our own (+ not just using the hyrax js) # we need to compile it in sprockets. @@ -139,6 +149,8 @@ gem 'slack-ruby-client', '~> 0.14.6' # https://github.com/rails/sprockets/issues/73#issuecomment-139113466 gem 'sprockets-es6', '~> 0.9.2' +gem 'valkyrie-shrine', '~> 1.0.0' + # Locking "redlock" to < 2.0, as the 2.x series currently breaks Sidekiq jobs. # @see https://github.com/samvera/hyrax/pull/5961 # @todo remove when Hyrax 3.5.1 or 3.6 (whichever includes it) drops @@ -148,12 +160,15 @@ gem 'redlock', '>= 0.1.2', '< 2.0' # that throws off how forms are built in Hyrax. gem 'simple_form', '< 5.2' +# sprockets support gets cagey after 1.0 +gem 'openseadragon', '~> 0.9.0' + # development dependencies (not as necessary to lock down versions here) group :development do # Seed data # gem 'ldr-development-seeds', github: 'LafayetteCollegeLibraries/ldr-development-seeds', branch: 'main' - gem 'listen', '>= 3.0.5', '< 3.8' + gem 'listen', '~> 3.9.0' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring', '~> 2.1.1' gem 'spring-watcher-listen', '~> 2.0.0' @@ -161,11 +176,11 @@ end # things used for development + testing (again, not as necessary to lock down versions) group :development, :test do - gem 'bixby', '~> 5.0.1' + gem 'bixby', '~> 5.0.2' gem 'byebug', '~> 11.1.3' gem 'capybara', '~> 3.38' gem 'capybara-screenshot', '~> 1.0.26' - gem 'database_cleaner', '~> 2.0.1' + gem 'database_cleaner', '~> 2.1.0' gem 'equivalent-xml', '~> 0.6.0', require: false gem 'factory_bot_rails', '~> 6', require: false gem 'hyrax-spec', '~> 0.3.2' @@ -173,8 +188,8 @@ group :development, :test do gem 'rspec', '~> 3.10' gem 'rspec-its', '~> 1.1' gem 'rspec_junit_formatter', '~> 0.4.1' - gem 'rspec-rails', '~> 5.1' - gem 'selenium-webdriver' + gem 'rspec-rails', '~> 6.1' + gem 'selenium-webdriver', '~> 4.31' gem 'shoulda-matchers', '~> 4' gem 'simplecov', '~> 0.22.0', require: false gem 'simplecov-cobertura', '~> 2.1', require: false diff --git a/Gemfile.lock b/Gemfile.lock index f10d88aa4..9d33c191c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,37 +1,53 @@ GEM remote: https://rubygems.org/ specs: - actioncable (5.2.8.1) - actionpack (= 5.2.8.1) + actioncable (6.1.7.10) + actionpack (= 6.1.7.10) + activesupport (= 6.1.7.10) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.8.1) - actionpack (= 5.2.8.1) - actionview (= 5.2.8.1) - activejob (= 5.2.8.1) + actionmailbox (6.1.7.10) + actionpack (= 6.1.7.10) + activejob (= 6.1.7.10) + activerecord (= 6.1.7.10) + activestorage (= 6.1.7.10) + activesupport (= 6.1.7.10) + mail (>= 2.7.1) + actionmailer (6.1.7.10) + actionpack (= 6.1.7.10) + actionview (= 6.1.7.10) + activejob (= 6.1.7.10) + activesupport (= 6.1.7.10) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.8.1) - actionview (= 5.2.8.1) - activesupport (= 5.2.8.1) - rack (~> 2.0, >= 2.0.8) + actionpack (6.1.7.10) + actionview (= 6.1.7.10) + activesupport (= 6.1.7.10) + rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.8.1) - activesupport (= 5.2.8.1) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.1.7.10) + actionpack (= 6.1.7.10) + activerecord (= 6.1.7.10) + activestorage (= 6.1.7.10) + activesupport (= 6.1.7.10) + nokogiri (>= 1.8.5) + actionview (6.1.7.10) + activesupport (= 6.1.7.10) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - active-fedora (13.3.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + active-fedora (15.0.1) active-triples (>= 0.11.0, < 2.0.0) - activemodel (>= 5.1) - activesupport (>= 5.1) + activemodel (>= 6.1) + activesupport (>= 6.1) deprecation - faraday (~> 0.12) + faraday (>= 1.0) faraday-encoding (>= 0.0.5) ldp (>= 0.7.0, < 2) + mutex_m rsolr (>= 1.1.2, < 3) ruby-progressbar (~> 1.0) active-triples (1.2.0) @@ -39,35 +55,38 @@ GEM activesupport (>= 3.0.0) rdf (>= 2.0.2, < 4.0) rdf-vocab (>= 2.0, < 4.0) - active_encode (0.8.2) + active_encode (1.3.0) + addressable (~> 2.8) rails - sprockets (< 4) - activejob (5.2.8.1) - activesupport (= 5.2.8.1) + activejob (6.1.7.10) + activesupport (= 6.1.7.10) globalid (>= 0.3.6) - activemodel (5.2.8.1) - activesupport (= 5.2.8.1) - activemodel-serializers-xml (1.0.2) - activemodel (> 5.x) - activesupport (> 5.x) + activemodel (6.1.7.10) + activesupport (= 6.1.7.10) + activemodel-serializers-xml (1.0.3) + activemodel (>= 5.0.0.a) + activesupport (>= 5.0.0.a) builder (~> 3.1) - activerecord (5.2.8.1) - activemodel (= 5.2.8.1) - activesupport (= 5.2.8.1) - arel (>= 9.0) + activerecord (6.1.7.10) + activemodel (= 6.1.7.10) + activesupport (= 6.1.7.10) activerecord-import (2.2.0) activerecord (>= 4.2) - activestorage (5.2.8.1) - actionpack (= 5.2.8.1) - activerecord (= 5.2.8.1) - marcel (~> 1.0.0) - activesupport (5.2.8.1) + activestorage (6.1.7.10) + actionpack (= 6.1.7.10) + activejob (= 6.1.7.10) + activerecord (= 6.1.7.10) + activesupport (= 6.1.7.10) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (6.1.7.10) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - addressable (2.8.5) - public_suffix (>= 2.0.2, < 6.0) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) almond-rails (0.3.0) rails (>= 4.2) anystyle (1.4.2) @@ -76,27 +95,29 @@ GEM namae (~> 1.0) wapiti (~> 2.1) anystyle-data (1.3.0) - arel (9.0.0) - ast (2.4.2) - autoprefixer-rails (10.4.13.0) + ast (2.4.3) + autoprefixer-rails (10.4.21.0) execjs (~> 2) - awesome_nested_set (3.5.0) - activerecord (>= 4.0.0, < 7.1) - aws-eventstream (1.3.0) - aws-partitions (1.887.0) - aws-sdk-core (3.191.0) + awesome_nested_set (3.8.0) + activerecord (>= 4.0.0, < 8.1) + aws-eventstream (1.4.0) + aws-partitions (1.1175.0) + aws-sdk-core (3.234.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.77.0) - aws-sdk-core (~> 3, >= 3.191.0) - aws-sigv4 (~> 1.1) + logger + aws-sdk-kms (1.115.0) + aws-sdk-core (~> 3, >= 3.234.0) + aws-sigv4 (~> 1.5) aws-sdk-s3 (1.142.0) aws-sdk-core (~> 3, >= 3.189.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babel-source (5.8.35) babel-transpiler (0.7.0) @@ -106,68 +127,68 @@ GEM docopt (~> 0.5.0) validatable (~> 1.6) base64 (0.3.0) - bcp47 (0.3.3) - i18n - bcrypt (3.1.19) - bibtex-ruby (6.0.0) + bcp47_spec (0.2.1) + bcrypt (3.1.20) + bibtex-ruby (6.2.0) latex-decode (~> 0.0) + logger (~> 1.7) + racc (~> 1.7) + bigdecimal (3.3.1) bixby (5.0.2) rubocop (= 1.28.2) rubocop-ast rubocop-performance rubocop-rails rubocop-rspec - blacklight (6.25.0) - bootstrap-sass (~> 3.2) + blacklight (7.41.0) deprecation globalid + hashdiff + i18n (>= 1.7.0) jbuilder (~> 2.7) kaminari (>= 0.15) - nokogiri (~> 1.6) - rails (>= 4.2, < 6) - rsolr (>= 1.0.6, < 3) - twitter-typeahead-rails (= 0.11.1.pre.corejavascript) - blacklight-access_controls (0.6.2) - blacklight (~> 6.0) - cancancan (~> 1.8) + ostruct (>= 0.3.2) + rails (>= 6.1, < 8.1) + view_component (>= 2.74, < 4) + zeitwerk + blacklight-access_controls (6.1.0) + blacklight (> 6.0, < 9) + cancancan (>= 1.8) deprecation (~> 1.0) - blacklight-gallery (0.12.0) - blacklight (~> 6.3) - bootstrap-sass (~> 3.0) - openseadragon (>= 0.2.0) - rails - blacklight_advanced_search (6.4.1) - blacklight (~> 6.0, >= 6.0.1) + blacklight-gallery (4.6.4) + blacklight (>= 7.17, < 9) + rails (>= 6.1, < 9) + blacklight_advanced_search (7.0.0) + blacklight (~> 7.0) parslet - blacklight_oai_provider (6.0.0) - blacklight (~> 6.0) - oai (~> 0.4) - rails (>= 4.2, < 6) - blacklight_range_limit (6.3.3) - blacklight (>= 6.0.2) - jquery-rails - rails (>= 3.0) - bootsnap (1.17.0) + blacklight_oai_provider (7.0.2) + blacklight (~> 7.0) + oai (~> 1.2) + rexml + blacklight_range_limit (8.5.0) + blacklight (>= 7.25.2, < 9) + deprecation + view_component (>= 2.54, < 4) + bootsnap (1.18.6) msgpack (~> 1.2) - bootstrap-sass (3.4.1) - autoprefixer-rails (>= 5.2.1) - sassc (>= 2.0.0) - bootstrap_form (5.1.0) - actionpack (>= 5.2) - activemodel (>= 5.2) + bootstrap (4.6.2.1) + autoprefixer-rails (>= 9.1.0) + popper_js (>= 1.16.1, < 2) + bootstrap_form (5.4.0) + actionpack (>= 6.1) + activemodel (>= 6.1) breadcrumbs_on_rails (3.0.1) - browse-everything (1.1.2) + browse-everything (1.6.0) addressable (~> 2.5) aws-sdk-s3 - dropbox_api (>= 0.1.10) - google-api-client (~> 0.23) - google_drive (>= 2.1, < 4) - googleauth (>= 0.6.6, < 1.0) - rails (>= 4.2, < 7.0) + dropbox_api (>= 0.1.20) + faraday (~> 2.0) + google-apis-drive_v3 + googleauth (>= 0.6.6, < 2.0) + rails (>= 4.2, < 8.1) ruby-box signet (~> 0.8) - typhoeus - builder (3.2.4) + builder (3.3.0) bulkrax (9.0.2) bagit (~> 0.6.0) coderay @@ -185,12 +206,12 @@ GEM rubyzip simple_form byebug (11.1.3) - cancancan (1.17.0) - capybara (3.39.2) + cancancan (3.6.1) + 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) @@ -198,11 +219,13 @@ GEM capybara-screenshot (1.0.26) capybara (>= 1.0, < 4) launchy - carrierwave (1.3.3) + carrierwave (1.3.4) activemodel (>= 4.0.0) activesupport (>= 4.0.0) mime-types (>= 1.16) ssrf_filter (~> 1.0, < 1.1.0) + childprocess (5.1.0) + logger (~> 1.5) clipboard-rails (1.7.1) coderay (1.1.3) coffee-rails (5.0.0) @@ -212,52 +235,50 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.2.3) - connection_pool (2.4.1) - crack (0.4.5) + concurrent-ruby (1.3.4) + connection_pool (2.5.4) + content_disposition (1.0.0) + crack (1.0.1) + bigdecimal rexml crass (1.0.6) csv (3.3.5) - database_cleaner (2.0.2) + database_cleaner (2.1.0) database_cleaner-active_record (>= 2, < 3) - database_cleaner-active_record (2.1.0) + database_cleaner-active_record (2.2.2) activerecord (>= 5.a) - database_cleaner-core (~> 2.0.0) + database_cleaner-core (~> 2.0) database_cleaner-core (2.0.1) - date (3.3.4) + date (3.4.1) declarative (0.0.20) - declarative-builder (0.1.0) - declarative-option (< 0.2.0) - declarative-option (0.1.0) denormalize_fields (1.3.0) activerecord (>= 4.1.14, < 8.0.0) deprecation (1.1.0) activesupport - devise (4.9.2) + devise (4.9.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-guests (0.8.1) + devise-guests (0.8.3) devise devise_cas_authenticatable (2.0.2) devise (>= 4.0.0) rack-cas - diff-lcs (1.5.0) - disposable (0.4.7) + diff-lcs (1.6.2) + disposable (0.6.3) declarative (>= 0.0.9, < 1.0.0) - declarative-builder (< 0.2.0) - declarative-option (< 0.2.0) - representable (>= 2.4.0, <= 3.1.0) - uber (< 0.2.0) + representable (>= 3.1.1, < 4) docile (1.4.1) docopt (0.5.0) dotenv (2.7.6) dotenv-rails (2.7.6) dotenv (= 2.7.6) railties (>= 3.2) - draper (4.0.2) + down (5.4.2) + addressable (~> 2.8) + draper (4.0.4) actionpack (>= 5.0) activemodel (>= 5.0) activemodel-serializers-xml (>= 1.0) @@ -267,67 +288,61 @@ GEM dropbox_api (0.1.21) faraday (< 3.0) oauth2 (~> 1.1) - dry-configurable (0.16.1) - dry-core (~> 0.6) + dry-configurable (1.3.0) + dry-core (~> 1.1) zeitwerk (~> 2.6) dry-container (0.11.0) concurrent-ruby (~> 1.0) - dry-core (0.9.1) + dry-core (1.1.0) concurrent-ruby (~> 1.0) + logger zeitwerk (~> 2.6) - dry-equalizer (0.3.0) - dry-events (0.2.0) + dry-events (1.1.0) concurrent-ruby (~> 1.0) - dry-core (~> 0.4) - dry-equalizer (~> 0.2) - dry-inflector (0.3.0) - dry-initializer (3.1.1) - dry-logic (1.3.0) + dry-core (~> 1.1) + dry-inflector (1.2.0) + dry-initializer (3.2.0) + dry-logic (1.6.0) + bigdecimal concurrent-ruby (~> 1.0) - dry-core (~> 0.9, >= 0.9) + dry-core (~> 1.1) zeitwerk (~> 2.6) - dry-matcher (0.9.0) - dry-core (~> 0.4, >= 0.4.8) - dry-monads (1.4.0) + dry-monads (1.9.0) concurrent-ruby (~> 1.0) - dry-core (~> 0.7) - dry-schema (1.11.3) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-schema (1.14.1) concurrent-ruby (~> 1.0) - dry-configurable (~> 0.16, >= 0.16) - dry-core (~> 0.9, >= 0.9) - dry-initializer (~> 3.0) - dry-logic (~> 1.3) - dry-types (~> 1.6) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.1) + dry-initializer (~> 3.2) + dry-logic (~> 1.5) + dry-types (~> 1.8) zeitwerk (~> 2.6) - dry-struct (1.5.2) - dry-core (~> 0.9, >= 0.9) - dry-types (~> 1.6) + dry-struct (1.8.0) + dry-core (~> 1.1) + dry-types (~> 1.8, >= 1.8.2) ice_nine (~> 0.11) zeitwerk (~> 2.6) - dry-transaction (0.13.3) - dry-container (>= 0.2.8) - dry-events (>= 0.1.0) - dry-matcher (>= 0.7.0) - dry-monads (>= 0.4.0) - dry-types (1.6.1) + dry-types (1.8.3) + bigdecimal (~> 3.0) concurrent-ruby (~> 1.0) - dry-container (~> 0.3) - dry-core (~> 0.9, >= 0.9) - dry-inflector (~> 0.1, >= 0.1.2) - dry-logic (~> 1.3, >= 1.3) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) zeitwerk (~> 2.6) - dry-validation (1.9.0) + dry-validation (1.11.1) concurrent-ruby (~> 1.0) - dry-container (~> 0.7, >= 0.7.1) - dry-core (~> 0.9, >= 0.9) - dry-initializer (~> 3.0) - dry-schema (~> 1.11, >= 1.11.0) + dry-core (~> 1.1) + dry-initializer (~> 3.2) + dry-schema (~> 1.14) zeitwerk (~> 2.6) - ebnf (2.3.5) + ebnf (2.6.0) + base64 (~> 0.2) htmlentities (~> 4.3) - rdf (~> 3.2) + rdf (~> 3.3) scanf (~> 1.0) - sxp (~> 1.2) + sxp (~> 2.0) unicode-types (~> 1.8) edtf (3.1.1) activesupport (>= 3.0, < 8.0) @@ -337,125 +352,153 @@ GEM roman (~> 0.2.0) equivalent-xml (0.6.0) nokogiri (>= 1.4.3) - erubi (1.12.0) - et-orbi (1.2.7) + erubi (1.13.1) + et-orbi (1.4.0) tzinfo - ethon (0.16.0) - ffi (>= 1.15.0) - execjs (2.8.1) - factory_bot (6.4.5) - activesupport (>= 5.0.0) - factory_bot_rails (6.4.3) - factory_bot (~> 6.4) - railties (>= 5.0.0) - faraday (0.17.6) - multipart-post (>= 1.2, < 3) - faraday-encoding (0.0.5) + execjs (2.10.0) + factory_bot (6.5.5) + activesupport (>= 6.1.0) + factory_bot_rails (6.5.1) + factory_bot (~> 6.5) + railties (>= 6.1.0) + faraday (2.14.0) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-encoding (0.0.6) faraday - faraday_middleware (0.14.0) - faraday (>= 0.7.4, < 1.0) - ffi (1.15.5) + faraday-follow_redirects (0.4.0) + faraday (>= 1, < 3) + faraday-mashify (1.0.0) + faraday (~> 2.0) + hashie + faraday-multipart (1.1.1) + multipart-post (~> 2.0) + faraday-net_http (3.4.1) + net-http (>= 0.5.0) + faraday-retry (2.3.2) + faraday (~> 2.0) + ffi (1.17.2) ffprober (1.0) sorbet-runtime - flipflop (2.7.1) + flipflop (2.8.0) activesupport (>= 4.0) terminal-table (>= 1.8) flot-rails (0.0.7) jquery-rails - font-awesome-rails (4.7.0.8) - railties (>= 3.2, < 8.0) - fugit (1.8.1) - et-orbi (~> 1, >= 1.2.7) + font-awesome-rails (4.7.0.9) + railties (>= 3.2, < 9.0) + fugit (1.12.1) + et-orbi (~> 1.4) raabro (~> 1.4) - gems (1.2.0) - geocoder (1.8.5) + gapic-common (1.2.0) + faraday (>= 1.9, < 3.a) + faraday-retry (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + google-protobuf (~> 4.26) + googleapis-common-protos (~> 1.6) + googleapis-common-protos-types (~> 1.15) + googleauth (~> 1.12) + grpc (~> 1.66) + geocoder (1.8.6) base64 (>= 0.1.0) csv (>= 3.0.0) - gli (2.21.0) - globalid (1.1.0) - activesupport (>= 5.0) - google-api-client (0.53.0) - google-apis-core (~> 0.1) - google-apis-generator (~> 0.1) - google-apis-core (0.11.0) - addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) - mini_mime (~> 1.0) + gli (2.22.2) + ostruct + globalid (1.3.0) + activesupport (>= 6.1) + google-analytics-data (0.7.2) + 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-cloud-errors (~> 1.0) + google-apis-core (1.0.2) + addressable (~> 2.8, >= 2.8.7) + faraday (~> 2.13) + faraday-follow_redirects (~> 0.3) + googleauth (~> 1.14) + mini_mime (~> 1.1) representable (~> 3.0) - retriable (>= 2.0, < 4.a) - rexml - webrick - google-apis-discovery_v1 (0.14.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-drive_v3 (0.42.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-generator (0.12.0) - activesupport (>= 5.0) - gems (~> 1.2) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-discovery_v1 (~> 0.5) - thor (>= 0.20, < 2.a) - google-apis-sheets_v4 (0.24.0) - google-apis-core (>= 0.11.0, < 2.a) - google_drive (3.0.7) - google-apis-drive_v3 (>= 0.5.0, < 1.0.0) - google-apis-sheets_v4 (>= 0.4.0, < 1.0.0) - googleauth (>= 0.5.0, < 1.0.0) - nokogiri (>= 1.5.3, < 2.0.0) - googleauth (0.17.1) - faraday (>= 0.17.3, < 2.0) - jwt (>= 1.4, < 3.0) - memoist (~> 0.16) + retriable (~> 3.1) + google-apis-drive_v3 (0.72.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.8.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-logging-utils (0.2.0) + google-protobuf (4.33.0) + bigdecimal + rake (>= 13) + googleapis-common-protos (1.9.0) + google-protobuf (~> 4.26) + googleapis-common-protos-types (~> 1.21) + grpc (~> 1.41) + googleapis-common-protos-types (1.22.0) + google-protobuf (~> 4.26) + googleauth (1.15.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) - signet (~> 0.15) + signet (>= 0.16, < 2.a) + grpc (1.75.0) + google-protobuf (>= 3.25, < 5.0) + googleapis-common-protos-types (~> 1.0) haml (6.3.0) temple (>= 0.8.2) thor tilt - hashdiff (1.0.1) + hashdiff (1.2.1) hashie (5.0.0) hiredis (0.6.3) honeybadger (4.12.2) htmlentities (4.3.4) - http_logger (0.7.0) - httpclient (2.8.3) - hydra-access-controls (11.0.7) + http_logger (1.0.1) + hydra-access-controls (13.1.0) active-fedora (>= 10.0.0) - activesupport (>= 4, < 6) - blacklight (>= 5.16) - blacklight-access_controls (~> 0.6.0) - cancancan (~> 1.8) + activesupport (>= 6.1, < 8.1) + blacklight-access_controls (~> 6.0) + cancancan (>= 1.8, < 4) deprecation (~> 1.0) - hydra-core (11.0.7) - hydra-access-controls (= 11.0.7) - railties (>= 4.0.0, < 6) - hydra-derivatives (3.7.0) - active-fedora (>= 11.5.6, != 13.2.1, != 13.2.0, != 13.1.3, != 13.1.2, != 13.1.1, != 13.1.0, != 13.0.0, != 12.2.1, != 12.2.0, != 12.1.1, != 12.1.0, != 12.0.3, != 12.0.2, != 12.0.1, != 12.0.0) - active_encode (~> 0.1) - activesupport (>= 4.0, < 7) + hydra-core (13.1.0) + hydra-access-controls (= 13.1.0) + railties (>= 6.1, < 8.1) + hydra-derivatives (4.0.0) + active-fedora (>= 14.0) + active-triples (>= 1.2) + active_encode (~> 1.2) + activesupport (>= 4.0, < 8.0) addressable (~> 2.5) deprecation mime-types (> 2.0, < 4.0) mini_magick (>= 3.2, < 5) - hydra-editor (5.0.5) + hydra-editor (7.0.0) active-fedora (>= 9.0.0) - activerecord (~> 5.0) + activerecord (>= 5.2, < 8.0) almond-rails (~> 0.1) - cancancan (~> 1.8) - rails (>= 5, < 6) - simple_form (>= 4.1.0, < 6.0) - sprockets (~> 3.7) + cancancan + concurrent-ruby (= 1.3.4) + psych (~> 3.3, < 4) + rails (>= 5.2, < 8.0) + simple_form (>= 4.1.0, < 5.2) + sprockets (>= 3.7) sprockets-es6 - hydra-file_characterization (1.1.2) + hydra-file_characterization (1.2.0) activesupport (>= 3.0.0) - hydra-head (11.0.7) - hydra-access-controls (= 11.0.7) - hydra-core (= 11.0.7) - rails (>= 5.2, < 6.1) - hydra-pcdm (1.3.0) - active-fedora (>= 10, < 15) + hydra-head (13.1.0) + hydra-access-controls (= 13.1.0) + hydra-core (= 13.1.0) + rails (>= 6.1, < 8.1) + hydra-pcdm (1.4.0) + active-fedora (>= 10) mime-types (>= 1) rdf-vocab hydra-role-management (1.1.0) @@ -465,54 +508,51 @@ GEM cancancan json (>= 1.8) psych (~> 3.0) - hydra-works (2.1.0) - activesupport (>= 5.2, < 7.1) - hydra-derivatives (~> 3.6) + hydra-works (2.2.0) + activesupport (>= 5.2, < 8.0) + hydra-derivatives (>= 3.6) hydra-file_characterization (~> 1.0) hydra-pcdm (>= 0.9) - hyrax (3.6.0) - active-fedora (~> 13.1, >= 13.1.2) + hyrax (5.2.0) + active-fedora (~> 15.0) almond-rails (~> 0.1) awesome_nested_set (~> 3.1) - blacklight (~> 6.14) - blacklight-gallery (~> 0.7) + blacklight (~> 7.29) + blacklight-gallery (~> 4.6.4) breadcrumbs_on_rails (~> 3.0) browse-everything (>= 0.16, < 2.0) carrierwave (~> 1.0) clipboard-rails (~> 1.5) + concurrent-ruby (= 1.3.4) + connection_pool (~> 2.4) draper (~> 4.0) - dry-equalizer (~> 0.2) - dry-events (~> 0.2.0) - dry-monads (< 1.5) - dry-struct (~> 1.0) - dry-transaction (~> 0.11) - dry-validation (~> 1.3) + dry-container (~> 0.11) + dry-events (~> 1.0, >= 1.0.1) + dry-logic (~> 1.5) + dry-monads (~> 1.6) + dry-validation (~> 1.10) flipflop (~> 2.3) flot-rails (~> 0.0.6) font-awesome-rails (~> 4.2) - hydra-derivatives (~> 3.3) - hydra-editor (~> 5.0, >= 5.0.4) - hydra-file_characterization (~> 1.1.2) - hydra-head (~> 11.0, >= 11.0.1) + google-analytics-data (~> 0.6) + hydra-derivatives (~> 4.0) + hydra-editor (~> 7.0) + hydra-file_characterization (~> 1.1) + hydra-head (~> 13.0) hydra-works (>= 0.16) iiif_manifest (>= 0.3, < 2.0) - jquery-datatables-rails (~> 3.4) - jquery-ui-rails (~> 6.0) - json-ld (< 3.2) json-schema - kaminari_route_prefix (~> 0.1.1) legato (~> 0.3) linkeddata + listen (~> 3.9) mailboxer (~> 0.12) nest (~> 3.1) - noid-rails (~> 3.0.0) + noid-rails (~> 3.0) oauth oauth2 (~> 1.2) - posix-spawn - power_converter (~> 0.1, >= 0.1.2) - psych (~> 3.3) + openseadragon (~> 0.9) qa (~> 5.5, >= 5.5.1) - rails (~> 5.0) + rails (> 6.1, < 8.0) rails_autolink (~> 1.1) rdf-rdfxml rdf-vocab (~> 3.0) @@ -522,51 +562,50 @@ GEM reform (~> 2.3) reform-rails (~> 0.2.0) retriable (>= 2.9, < 4.0) - samvera-nesting_indexer (~> 2.0) - sass-rails (~> 5.0) + sass-rails (~> 6.0) select2-rails (~> 3.5) signet + sprockets (= 3.7.2) tinymce-rails (~> 5.10) - valkyrie (~> 2, >= 2.1.1) + valkyrie (~> 3.5) + view_component (~> 2.74.1) hyrax-spec (0.3.2) rspec (~> 3.6) - i18n (1.14.4) + i18n (1.14.7) concurrent-ruby (~> 1.0) ice_nine (0.11.2) - iiif_manifest (1.3.1) + iiif_manifest (1.6.0) activesupport (>= 4) - iso-639 (0.3.6) + io-console (0.8.1) + iso-639 (0.3.8) + csv iso8601 (0.9.1) jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.2) - jquery-datatables-rails (3.4.0) - actionpack (>= 3.1) - jquery-rails - railties (>= 3.1) - sass-rails - jquery-rails (4.6.0) + jquery-rails (4.6.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-ui-rails (6.0.1) - railties (>= 3.2.16) - json (2.6.3) - json-canonicalization (0.4.0) - json-ld (3.1.10) + json (2.15.1) + json-canonicalization (1.0.0) + json-ld (3.3.2) htmlentities (~> 4.3) - json-canonicalization (~> 0.2) + json-canonicalization (~> 1.0) link_header (~> 0.0, >= 0.0.8) - multi_json (~> 1.14) - rack (~> 2.0) - rdf (~> 3.1) - json-ld-preloaded (3.1.6) - json-ld (~> 3.1) - rdf (~> 3.1) - json-schema (4.0.0) - addressable (>= 2.8) - jwt (2.7.1) + multi_json (~> 1.15) + rack (>= 2.2, < 4) + rdf (~> 3.3) + rexml (~> 3.2) + json-ld-preloaded (3.3.2) + json-ld (~> 3.3) + rdf (~> 3.3) + json-schema (6.0.0) + addressable (~> 2.8) + bigdecimal (~> 3.1) + jwt (2.10.2) + base64 kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -579,25 +618,26 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - kaminari_route_prefix (0.1.1) - kaminari (~> 1.0) language_list (1.2.1) latex-decode (0.4.0) - launchy (2.5.2) + launchy (3.1.1) addressable (~> 2.8) - ld-patch (3.2.0) - ebnf (~> 2.2) - rdf (~> 3.2) - rdf-xsd (~> 3.2) - sparql (~> 3.2) - sxp (~> 1.2) - ldp (1.0.3) + childprocess (~> 5.0) + logger (~> 1.6) + ld-patch (3.3.1) + ebnf (~> 2.6) + rdf (~> 3.3) + rdf-xsd (~> 3.3) + sparql (~> 3.3) + sxp (~> 2.0) + ldp (1.2.0) deprecation - faraday + faraday (>= 1) http_logger - json-ld - rdf (>= 1.1) + json-ld (~> 3.2) + rdf (~> 3.2) rdf-isomorphic + rdf-ldp rdf-turtle rdf-vocab (>= 0.8) slop @@ -608,7 +648,7 @@ GEM rdf-vocab (~> 3.0) legato (0.7.0) multi_json - libxml-ruby (5.0.3) + libxml-ruby (5.0.5) link_header (0.0.8) linkeddata (3.1.6) equivalent-xml (~> 0.6) @@ -637,19 +677,20 @@ GEM shex (~> 0.6, >= 0.6.4) sparql (~> 3.1, >= 3.1.8) sparql-client (~> 3.1, >= 3.1.2) - listen (3.7.1) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - logger (1.5.3) + logger (1.7.0) lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.22.0) + loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) - mail (2.8.1) + mail (2.9.0) + logger mini_mime (>= 0.1.1) net-imap net-pop @@ -657,78 +698,85 @@ GEM mailboxer (0.15.1) carrierwave (>= 0.5.8) rails (>= 5.0.0) - marcel (1.0.4) - matrix (0.4.2) - memoist (0.16.2) - method_source (1.0.0) - mime-types (3.4.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2023.0218.1) - mini_magick (4.12.0) + marcel (1.1.0) + 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.2025.0924) + mini_magick (4.13.2) mini_mime (1.1.5) - mini_portile2 (2.8.5) - minitest (5.22.3) - msgpack (1.7.1) - multi_json (1.15.0) - multi_xml (0.6.0) - multipart-post (2.4.0) - namae (1.1.1) + mini_portile2 (2.8.9) + minitest (5.26.0) + msgpack (1.8.0) + multi_json (1.17.0) + multi_xml (0.7.2) + bigdecimal (~> 3.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + namae (1.2.0) + racc (~> 1.7) nest (3.2.0) redic - net-http-persistent (4.0.2) - connection_pool (~> 2.2) - net-imap (0.4.10) + net-http (0.6.0) + uri + net-http-persistent (4.0.6) + connection_pool (~> 2.2, >= 2.2.4) + net-imap (0.5.12) date net-protocol net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout - net-smtp (0.4.0.1) + net-smtp (0.5.1) net-protocol - nio4r (2.7.0) + nio4r (2.7.4) noid (0.9.0) - noid-rails (3.0.3) - actionpack (>= 5.0.0, < 7) + noid-rails (3.3.0) + actionpack (>= 5.0.0, < 9) noid (~> 0.9) - nokogiri (1.15.6) + nokogiri (1.18.10) mini_portile2 (~> 2.8.2) racc (~> 1.4) non-digest-assets (2.2.0) activesupport (>= 5.2, < 7.1) sprockets (>= 2.0, < 5.0) - oai (0.4.0) + oai (1.3.0) builder (>= 3.1.0) - faraday - faraday_middleware - oauth (1.1.0) - oauth-tty (~> 1.0, >= 1.0.1) + 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) - oauth-tty (1.0.5) - version_gem (~> 1.1, >= 1.1.1) + version_gem (~> 1.1, >= 1.1.9) + oauth-tty (1.0.6) + version_gem (~> 1.1, >= 1.1.9) oauth2 (1.4.11) faraday (>= 0.17.3, < 3.0) jwt (>= 1.0, < 3.0) multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 4) - okcomputer (1.18.5) - openseadragon (0.6.0) - rails (> 3.2.0) + okcomputer (1.18.6) + openseadragon (0.9.0) + rails (> 6.1.0) orm_adapter (0.5.0) os (1.1.4) - parallel (1.23.0) - parser (3.2.2.3) + ostruct (0.6.3) + parallel (1.27.0) + parser (3.3.9.0) ast (~> 2.4.1) racc parslet (2.0.0) - pg (1.5.4) - posix-spawn (0.3.15) - power_converter (0.1.2) + pg (1.5.9) + popper_js (1.16.1) + prism (1.6.0) psych (3.3.4) - public_suffix (5.0.3) - puma (6.4.2) + public_suffix (6.0.2) + puma (6.4.3) nio4r (~> 2.0) qa (5.15.0) activerecord-import @@ -740,115 +788,130 @@ GEM rails (>= 5.0, < 8.1) rdf raabro (1.4.0) - racc (1.7.3) - rack (2.2.8.1) + racc (1.8.1) + rack (2.2.20) rack-cas (0.16.1) addressable (~> 2.3) nokogiri (~> 1.5) rack (>= 1.3) - rack-protection (3.0.6) - rack - rack-test (2.1.0) + rack-protection (3.2.0) + base64 (>= 0.1.0) + rack (~> 2.2, >= 2.2.4) + rack-test (2.2.0) rack (>= 1.3) - rails (5.2.8.1) - actioncable (= 5.2.8.1) - actionmailer (= 5.2.8.1) - actionpack (= 5.2.8.1) - actionview (= 5.2.8.1) - activejob (= 5.2.8.1) - activemodel (= 5.2.8.1) - activerecord (= 5.2.8.1) - activestorage (= 5.2.8.1) - activesupport (= 5.2.8.1) - bundler (>= 1.3.0) - railties (= 5.2.8.1) + rails (6.1.7.10) + actioncable (= 6.1.7.10) + actionmailbox (= 6.1.7.10) + actionmailer (= 6.1.7.10) + actionpack (= 6.1.7.10) + actiontext (= 6.1.7.10) + actionview (= 6.1.7.10) + activejob (= 6.1.7.10) + activemodel (= 6.1.7.10) + activerecord (= 6.1.7.10) + activestorage (= 6.1.7.10) + activesupport (= 6.1.7.10) + bundler (>= 1.15.0) + railties (= 6.1.7.10) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.2.0) + rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) + rails-html-sanitizer (1.6.2) loofah (~> 2.21) - nokogiri (~> 1.14) + 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) activesupport (> 3.1) railties (> 3.1) - railties (5.2.8.1) - actionpack (= 5.2.8.1) - activesupport (= 5.2.8.1) + railties (6.1.7.10) + actionpack (= 6.1.7.10) + activesupport (= 6.1.7.10) method_source - rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) + rake (>= 12.2) + thor (~> 1.0) rainbow (3.1.1) - rake (13.1.0) + rake (13.3.0) rb-fsevent (0.11.2) - rb-inotify (0.10.1) + rb-inotify (0.11.1) ffi (~> 1.0) - rdf (3.2.11) + rdf (3.3.4) + bcp47_spec (~> 0.2) + bigdecimal (~> 3.1, >= 3.1.5) link_header (~> 0.0, >= 0.0.8) - rdf-aggregate-repo (3.2.1) - rdf (~> 3.2) - rdf-isomorphic (3.2.1) - rdf (~> 3.2) - rdf-json (3.2.0) - rdf (~> 3.2) - rdf-microdata (3.2.1) + logger (~> 1.5) + ostruct (~> 0.6) + readline (~> 0.0) + rdf-aggregate-repo (3.3.0) + rdf (~> 3.3) + rdf-isomorphic (3.3.0) + rdf (~> 3.3) + rdf-json (3.3.0) + rdf (~> 3.3) + rdf-ldp (0.1.0) + deprecation + rdf + 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.2) - 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.1.1) - addressable (~> 2.3) - bcp47 (~> 0.3, >= 0.3.3) - json-ld (~> 3.1) - rdf (~> 3.1) - rdf-vocab (~> 3.1) - rdf-xsd (~> 3.1) - rdf-trig (3.2.0) - ebnf (~> 2.2) - rdf (~> 3.2) - rdf-turtle (~> 3.2) - rdf-trix (3.2.0) - rdf (~> 3.2) - rdf-xsd (~> 3.2) - rdf-turtle (3.2.1) - ebnf (~> 2.3) - rdf (~> 3.2) - rdf-vocab (3.2.7) - rdf (~> 3.2, >= 3.2.4) - rdf-xsd (3.2.1) - rdf (~> 3.2) + 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_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.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.3.0) + rdf (~> 3.3) rexml (~> 3.2) + readline (0.0.4) + reline redic (1.5.3) hiredis redis (4.5.1) @@ -856,53 +919,55 @@ GEM redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - reform (2.5.0) - disposable (>= 0.4.2, < 0.5.0) - representable (>= 2.4.0, < 3.1.0) + reform (2.6.2) + disposable (>= 0.5.0, < 1.0.0) + representable (>= 3.1.1, < 4) uber (< 0.2.0) - reform-rails (0.2.5) + reform-rails (0.2.6) activemodel (>= 5.0) reform (>= 2.3.1, < 3.0.0) - regexp_parser (2.8.1) - representable (3.0.4) + regexp_parser (2.11.3) + reline (0.6.2) + io-console (~> 0.5) + representable (3.2.0) declarative (< 0.1.0) - declarative-option (< 0.2.0) + trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - request_store (1.5.1) + request_store (1.7.0) rack (>= 1.4) - responders (3.1.0) + responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) retriable (3.1.2) - rexml (3.2.6) + 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.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-its (1.3.0) + rspec-support (~> 3.13.0) + rspec-its (1.3.1) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.12.6) + rspec-mocks (3.13.6) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-rails (5.1.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) - rspec-support (3.12.1) + rspec-support (~> 3.13.0) + rspec-rails (6.1.5) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.6) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) rubocop (1.28.2) @@ -914,9 +979,10 @@ GEM rubocop-ast (>= 1.17.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.29.0) - parser (>= 3.2.1.0) - rubocop-performance (1.18.0) + rubocop-ast (1.47.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.19.1) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) rubocop-rails (2.15.2) @@ -933,43 +999,43 @@ GEM ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) rubyzip (2.3.2) - samvera-nesting_indexer (2.0.0) - dry-equalizer - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.1.0) - railties (>= 5.2.0) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) + sass-rails (6.0.0) + sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt scanf (1.0.0) select2-rails (3.5.11) - selenium-webdriver (4.9.0) + selenium-webdriver (4.37.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.1.1) - json-ld (~> 3.1, >= 3.1.7) - rdf (~> 3.1, >= 3.1.8) - sparql (~> 3.1) - sxp (~> 1.1) - shex (0.6.4) - ebnf (~> 2.1, >= 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.1) - json-ld-preloaded (~> 3.1) - rdf (~> 3.1) - rdf-xsd (~> 3.1) - sparql (~> 3.1) - sxp (~> 1.1) + 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.6.0) + content_disposition (~> 1.0) + down (~> 5.1) sidekiq (5.2.10) connection_pool (~> 2.2, >= 2.2.2) rack (~> 2.0) @@ -978,10 +1044,10 @@ GEM sidekiq-cron (1.9.1) fugit (~> 1.8) sidekiq (>= 4.2.1) - signet (0.17.0) + signet (0.21.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) + jwt (>= 1.5, < 4.0) multi_json (~> 1.10) simple_form (5.1.0) actionpack (>= 5.2) @@ -993,32 +1059,33 @@ GEM simplecov-cobertura (2.1.0) rexml simplecov (~> 0.19) - simplecov-html (0.12.3) + simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) - slack-ruby-client (0.14.6) - activesupport - faraday (>= 0.9) - faraday_middleware + slack-ruby-client (3.0.0) + faraday (>= 2.0.1) + faraday-mashify + faraday-multipart gli hashie - websocket-driver + logger slop (4.10.1) - snaky_hash (2.0.1) - hashie - version_gem (~> 1.1, >= 1.1.1) - sorbet-runtime (0.5.11463) - sparql (3.2.5) - builder (~> 3.2) - ebnf (~> 2.2, >= 2.3.1) + snaky_hash (2.0.3) + hashie (>= 0.1.0, < 6) + version_gem (>= 1.1.8, < 3) + sorbet-runtime (0.6.12656) + sparql (3.3.2) + builder (~> 3.2, >= 3.2.4) + ebnf (~> 2.5) logger (~> 1.5) - rdf (~> 3.2, >= 3.2.8) - rdf-aggregate-repo (~> 3.2) - rdf-xsd (~> 3.2) - sparql-client (~> 3.2, >= 3.2.1) - sxp (~> 1.2, >= 1.2.2) - sparql-client (3.2.1) - net-http-persistent (~> 4.0, >= 4.0.1) - rdf (~> 3.2, >= 3.2.6) + 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.3) spring (2.1.1) spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) @@ -1037,43 +1104,41 @@ GEM ssrf_filter (1.0.8) stub_env (1.0.4) rspec (>= 2.0, < 4.0) - sxp (1.2.4) + sxp (2.0.0) matrix (~> 0.4) - rdf (~> 3.2) - temple (0.10.3) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - thor (1.3.1) - thread_safe (0.3.6) - tilt (2.3.0) - timeout (0.4.1) - tinymce-rails (5.10.7.1) + rdf (~> 3.3) + temple (0.10.4) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + thor (1.4.0) + tilt (2.6.1) + timeout (0.4.3) + tinymce-rails (5.10.9) railties (>= 3.1.1) + trailblazer-option (0.1.2) turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) - twitter-typeahead-rails (0.11.1.pre.corejavascript) + twitter-typeahead-rails (0.11.1) actionpack (>= 3.1) jquery-rails railties (>= 3.1) - typhoeus (1.4.0) - ethon (>= 0.9.0) - tzinfo (1.2.11) - thread_safe (~> 0.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) uber (0.1.0) - uglifier (4.2.0) + uglifier (4.2.1) execjs (>= 0.3.0, < 3) - unicode-display_width (2.4.2) - unicode-types (1.8.0) + unicode-display_width (2.6.0) + unicode-types (1.11.0) + uri (1.0.4) validatable (1.6.7) - valkyrie (2.2.0) + valkyrie (3.5.0) activemodel activesupport - disposable (~> 0.4.5) - draper dry-struct dry-types (~> 1.0) - faraday (< 1.0) + faraday (>= 0.9, < 3, != 2.0.0) + faraday-multipart json json-ld railties @@ -1081,24 +1146,32 @@ GEM rdf-vocab reform (~> 2.2) reform-rails - version_gem (1.1.3) + valkyrie-shrine (1.0.0) + aws-sdk-s3 (~> 1) + shrine (>= 2.0, < 4.0) + valkyrie (> 1.0) + version_gem (1.1.9) + view_component (2.74.1) + activesupport (>= 5.0.0, < 8.0) + concurrent-ruby (~> 1.0) + method_source (~> 1.0) wapiti (2.1.0) builder (~> 3.2) rexml (~> 3.0) warden (1.2.9) rack (>= 2.0.9) - webmock (3.19.1) + webmock (3.25.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.8.1) - websocket (1.2.10) - websocket-driver (0.7.6) + websocket (1.2.11) + websocket-driver (0.8.0) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.13) + zeitwerk (2.7.3) PLATFORMS ruby @@ -1108,18 +1181,19 @@ DEPENDENCIES anystyle (~> 1.4.1) aws-sdk-s3 (~> 1.142.0) bagit (~> 0.6.0) - bixby (~> 5.0.1) - blacklight_advanced_search (~> 6.4.1) - blacklight_oai_provider (~> 6.0.0) - blacklight_range_limit (~> 6.3.3) + bixby (~> 5.0.2) + blacklight_advanced_search (~> 7.0.0) + blacklight_oai_provider (~> 7.0.2) + blacklight_range_limit (~> 8.5.0) bootsnap (~> 1.17) - browse-everything (~> 1.1.2) + bootstrap (~> 4.0) + browse-everything (~> 1.6.0) bulkrax (~> 9.0.2) byebug (~> 11.1.3) capybara (~> 3.38) capybara-screenshot (~> 1.0.26) coffee-rails (~> 5.0.0) - database_cleaner (~> 2.0.1) + database_cleaner (~> 2.1.0) devise (~> 4.9.0) devise-guests (~> 0.8.1) devise_cas_authenticatable (~> 2.0.2) @@ -1128,49 +1202,54 @@ DEPENDENCIES edtf-humanize (~> 2.1.0) equivalent-xml (~> 0.6.0) factory_bot_rails (~> 6) - faraday (~> 0.17.6) - ffprober + faraday (~> 2.0) + ffprober (~> 1.0) honeybadger (~> 4.12.1) hydra-role-management (~> 1.1.0) - hyrax (~> 3.6.0) + hyrax (~> 5.2.0) hyrax-spec (~> 0.3.2) iso-639 (~> 0.3.6) jbuilder (~> 2.11.5) jquery-rails (~> 4.6.0) kaminari (~> 1.2.2) linkeddata (~> 3.1.6) - listen (>= 3.0.5, < 3.8) + listen (~> 3.9.0) lograge - mini_magick (~> 4.11) + mini_magick (~> 4.13.2) non-digest-assets (~> 2.2.0) okcomputer (~> 1.18.5) + openseadragon (~> 0.9.0) pg (~> 1.5.4) puma (~> 6.4.0) - rails (~> 5.2.7) + rails (~> 6.1) rails-controller-testing (~> 1.0.5) - rdf-vocab (~> 3.2.7) + rdf-vocab (~> 3.3.3) redlock (>= 0.1.2, < 2.0) rsolr (~> 2.5.0) rspec (~> 3.10) rspec-its (~> 1.1) - rspec-rails (~> 5.1) + rspec-rails (~> 6.1) rspec_junit_formatter (~> 0.4.1) rubyzip (~> 2.3.2) - sass-rails (~> 5.1.0) - selenium-webdriver + sass-rails (~> 6.0) + selenium-webdriver (~> 4.31) shoulda-matchers (~> 4) sidekiq (~> 5.2.9) sidekiq-cron (~> 1.9.1) simple_form (< 5.2) simplecov (~> 0.22.0) simplecov-cobertura (~> 2.1) - slack-ruby-client (~> 0.14.6) + slack-ruby-client spring (~> 2.1.1) spring-watcher-listen (~> 2.0.0) + sprockets (= 3.7.2) sprockets-es6 (~> 0.9.2) + sprockets-rails (= 3.4.2) stub_env (~> 1.0.4) turbolinks (~> 5.2.1) + twitter-typeahead-rails (~> 0.11.1) uglifier (~> 4.2.0) + valkyrie-shrine (~> 1.0.0) webmock (~> 3.8) BUNDLED WITH diff --git a/app/actors/concerns/deserializes_rdf_literals.rb b/app/actors/concerns/deserializes_rdf_literals.rb index 9b78d8e65..eb66399d3 100644 --- a/app/actors/concerns/deserializes_rdf_literals.rb +++ b/app/actors/concerns/deserializes_rdf_literals.rb @@ -87,7 +87,7 @@ def deserialize(value) # @return [RdfLiteralSerializer] def serializer - @serializer ||= RdfLiteralSerializer.new + RdfLiteralSerializer end # Fetches contents of the +language_tagged_fields+ singleton method defined diff --git a/app/actors/solr_suggest_actor.rb b/app/actors/solr_suggest_actor.rb index f9e7400f6..66f17be1c 100644 --- a/app/actors/solr_suggest_actor.rb +++ b/app/actors/solr_suggest_actor.rb @@ -1,4 +1,10 @@ # frozen_string_literal: true +# +# Updates the Solr suggestion dictionaries after a work is modified +# so that they're up-to-date with values. +# +# @note replaced in Hyrax::Transactions with Spot::Listeners::SolrSuggestDictionaryListener +# class SolrSuggestActor < ::Hyrax::Actors::AbstractActor # @param [Hyrax::Actors::Environment] env # @return [void] diff --git a/app/actors/spot/actors/collections_membership_actor.rb b/app/actors/spot/actors/collections_membership_actor.rb index 22f414f76..da6102090 100644 --- a/app/actors/spot/actors/collections_membership_actor.rb +++ b/app/actors/spot/actors/collections_membership_actor.rb @@ -26,8 +26,6 @@ def add(env, id) col = collection_stack.shift next if collection_ids.include?(col.id) - col.reindex_extent = Hyrax::Adapters::NestingIndexAdapter::LIMITED_REINDEX - collection_ids << col.id env.curation_concern.member_of_collections << col collection_stack += col.member_of_collections diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index b16e53d6d..75466dd08 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1,3 +1,5 @@ //= link_tree ../images //= link_directory ../javascripts .js //= link_directory ../stylesheets .css + +//= link blacklight_gallery/manifest.js \ No newline at end of file diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 7e6b2f8dc..10049a77d 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -13,18 +13,24 @@ //= require turbolinks // Required by Blacklight -//= require jquery -//= require blacklight_advanced_search -//= require blacklight_range_limit -//= require jquery_ujs -//= require dataTables/jquery.dataTables -//= require dataTables/bootstrap/3/jquery.dataTables.bootstrap +//= require jquery3 +//= require rails-ujs + +//= require popper +//= require twitter/typeahead +//= require bootstrap + +//= require jquery.dataTables +//= require dataTables.bootstrap4 //= require blacklight/blacklight -// local require that in turn calls a require -//= require blacklight_gallery +//= require blacklight_gallery/default +//= require blacklight_gallery/osd_viewer + +// require openseadragon +//= require openseadragon/openseadragon +//= require openseadragon/rails -//= require openseadragon //= require hyrax //= require almond diff --git a/app/assets/javascripts/blacklight_gallery.js b/app/assets/javascripts/blacklight_gallery.js deleted file mode 100644 index 992a7d4df..000000000 --- a/app/assets/javascripts/blacklight_gallery.js +++ /dev/null @@ -1 +0,0 @@ -//= require blacklight_gallery/default \ No newline at end of file diff --git a/app/assets/javascripts/openseadragon.js b/app/assets/javascripts/openseadragon.js deleted file mode 100644 index 2ab91a9f1..000000000 --- a/app/assets/javascripts/openseadragon.js +++ /dev/null @@ -1,2 +0,0 @@ -//= require openseadragon/openseadragon -//= require openseadragon/rails \ No newline at end of file diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 5e6f25936..771a689b2 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -15,8 +15,8 @@ // //= require hyrax //= require openseadragon -//= require dataTables/bootstrap/3/jquery.dataTables.bootstrap -//= require 'bulkrax/application' +//= require dataTables.bootstrap4 +//= require bulkrax/application // //= require spot //= require_self diff --git a/app/assets/stylesheets/hyrax.scss b/app/assets/stylesheets/hyrax.scss index 2baaa74e2..1efe4ecd5 100644 --- a/app/assets/stylesheets/hyrax.scss +++ b/app/assets/stylesheets/hyrax.scss @@ -1,14 +1,13 @@ // our own overrides are called _before_ we import hyrax/bootstrap @import "spot/variables"; -@import "bootstrap-compass"; -@import "bootstrap-sprockets"; +// @import "bootstrap-compass"; @import "bootstrap-default-overrides"; -@import 'bootstrap'; -@import 'blacklight/blacklight'; +@import "bootstrap"; +@import "blacklight/blacklight"; @import "font-awesome"; -@import "blacklight_gallery/gallery"; @import "blacklight_gallery/masonry"; @import "blacklight_gallery/slideshow"; @import "blacklight_gallery/osd_viewer"; +@import "hyrax/blacklight_gallery"; @import 'hyrax/hyrax'; diff --git a/app/assets/stylesheets/spot.scss b/app/assets/stylesheets/spot.scss index 7d6e538b3..43b6fd3c2 100644 --- a/app/assets/stylesheets/spot.scss +++ b/app/assets/stylesheets/spot.scss @@ -24,12 +24,12 @@ } [role="main"] { - margin-top: 1rem; + margin-top: 2rem; padding-bottom: 10rem; } .dashboard > [role="main"] { - margin-top: 0; + margin-top: 1.25rem; } .thumbnail.file-set { @@ -76,7 +76,8 @@ .site-footer { background-color: $tan-light; - height: 20rem; + font-size: .85rem; + min-height: 180px; } // duplicating styles from #facet-panel-collapse @@ -138,3 +139,7 @@ select[multiple] { } } } + +#metadata .form-group label { + font-weight: 700; +} diff --git a/app/assets/stylesheets/spot/_homepage.scss b/app/assets/stylesheets/spot/_homepage.scss index 3e7d25779..be642ec2f 100644 --- a/app/assets/stylesheets/spot/_homepage.scss +++ b/app/assets/stylesheets/spot/_homepage.scss @@ -5,6 +5,7 @@ $featured-info-text-color: $white; background-position: top center; background-size: cover; min-height: 50rem; + padding-bottom: 4rem; position: relative; } diff --git a/app/assets/stylesheets/spot/_navbar.scss b/app/assets/stylesheets/spot/_navbar.scss index 843bc9999..01b510a05 100644 --- a/app/assets/stylesheets/spot/_navbar.scss +++ b/app/assets/stylesheets/spot/_navbar.scss @@ -1,9 +1,12 @@ // styles applied to the navbar + masthead -#masthead { // scss-lint:disable IdSelector - background-color: $red-light; +#masthead.navbar.bg-dark { // scss-lint:disable IdSelector + background-color: $red-light !important; border-bottom: 0; + .nav-item a { color: $navbar-inverse-link-color; } + .nav-item a.dropdown-item { color: #212529; } + @media(min-width: 768px) { #search-field-header { // scss-lint:disable IdSelector width: 50rem; diff --git a/app/assets/stylesheets/spot/_variables.scss b/app/assets/stylesheets/spot/_variables.scss index 8f202a4a1..ecc9d80f6 100644 --- a/app/assets/stylesheets/spot/_variables.scss +++ b/app/assets/stylesheets/spot/_variables.scss @@ -9,9 +9,7 @@ $tan-light: #e8e6e2; $white: #fff; $black: #1e1e1e; -//// -// bootstrap overrides -//// - // navbar / masthead $navbar-inverse-link-color: $white; + +$font-size-base: 0.85rem; diff --git a/app/authorities/concerns/spot/qa_assign_fast_generic_authority_patch.rb b/app/authorities/concerns/spot/qa_assign_fast_generic_authority_patch.rb new file mode 100644 index 000000000..c2c3f4580 --- /dev/null +++ b/app/authorities/concerns/spot/qa_assign_fast_generic_authority_patch.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +module Spot + module QaAssignFastGenericAuthorityPatch + extend ActiveSupport::Concern + + private + + def parse_authority_response(raw_response) + results = raw_response.try(:[], 'response').try(:[], 'docs') || [] + + results.map do |doc| + index = Qa::Authorities::AssignFast.index_for_authority(subauthority) + term = doc[index].first + term += " (USE #{doc['auth']})" if doc['type'] == 'alt' + fast_id = Array.wrap(doc['idroot']).first + + { + fast_id: fast_id, + id: "http://id.worldcat.org/fast/#{fast_id.gsub(/^fst/, '')}", + label: term, + type: doc['type'], + value: doc['auth'] + } + end + end + end +end diff --git a/app/authorities/concerns/spot/qa_assign_fast_subauthority_patch.rb b/app/authorities/concerns/spot/qa_assign_fast_subauthority_patch.rb new file mode 100644 index 000000000..1d95273f2 --- /dev/null +++ b/app/authorities/concerns/spot/qa_assign_fast_subauthority_patch.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +module Spot + module QaAssignFastSubauthorityPatch + extend ActiveSupport::Concern + + def index_for_authority(authority) + return authority if authority == 'idroot' + + Qa::Authorities::AssignFastSubauthority::SUBAUTHORITIES[authority] + end + + def subauthorities + Qa::Authorities::AssignFastSubauthority::SUBAUTHORITIES.keys + ['idroot'] + end + end +end diff --git a/app/authorities/qa/authorities/language.rb b/app/authorities/qa/authorities/language.rb index 9c1a1b04d..0ac36026d 100644 --- a/app/authorities/qa/authorities/language.rb +++ b/app/authorities/qa/authorities/language.rb @@ -1,30 +1,30 @@ # frozen_string_literal: true # # A light class used to provide a +questioning_authority+ endpoint -# to search ISO-639-1 languages. See {Spot::ISO6391} for details. +# to search ISO-639-1 languages. See {Spot::Iso6391} for details. module Qa::Authorities class Language < Qa::Authorities::Base class_attribute :all - # All of the languages available from the {Spot::ISO6391} service mapped + # All of the languages available from the {Spot::Iso6391} service mapped # to a JSON format that QA expects. # # @return [Array String>>] def all - Spot::ISO6391.all.map { |key, val| wrap(id: key, label: val) }.compact + Spot::Iso6391.all.map { |key, val| wrap(id: key, label: val) }.compact end # @param [String] id # @return [Hash String>, NilClass] def find(id) - wrap(id: id, label: Spot::ISO6391.label_for(id)) + wrap(id: id, label: Spot::Iso6391.label_for(id)) end # @param [String] query # @return [Array String>>] def search(query) q_reg = Regexp.new(query.downcase, :i) - Spot::ISO6391.all + Spot::Iso6391.all .select { |_key, label| label.match? q_reg } .map { |key, label| wrap(id: key, label: label) } .compact # just in case diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index fa502e1f0..3d71e09ec 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -13,9 +13,6 @@ class ApplicationController < ActionController::Base before_action :store_user_location!, if: :storable_location? - # from Blacklight: 'Discarding flash messages on XHR requests is deprecated.' - skip_after_action :discard_flash_if_xhr - protect_from_forgery with: :exception # @return [Hash] diff --git a/app/controllers/catalog_controller.rb b/app/controllers/catalog_controller.rb index ba1e4a675..5b7751b50 100644 --- a/app/controllers/catalog_controller.rb +++ b/app/controllers/catalog_controller.rb @@ -14,6 +14,8 @@ class CatalogController < ApplicationController before_action :enforce_show_permissions, only: :show configure_blacklight do |config| + config.http_method = Hyrax.config.solr_default_method + # default advanced config values config.advanced_search ||= Blacklight::OpenStructWithHashAccess.new # config.advanced_search[:qt] ||= 'advanced' @@ -163,12 +165,27 @@ class CatalogController < ApplicationController # config.add_search_field('all_fields', label: 'All Fields') do |field| fields = %w[ - title_tesim subtitle_tesim title_alternative_tesim - creator_tesim contributor_tesim publisher_tesim editor_tesim - source_tesim abstract_tesim description_tesim note_tesim - subject_label_tesim identifier_ssim bibliographic_citation_tesim - english_language_date_teim file_format_tesim - extracted_text_tsimv keyword_tesim date_associated_tesim subject_ocm_tesim + title_tesim + subtitle_tesim + title_alternative_tesim + creator_tesim + contributor_tesim + publisher_tesim + editor_tesim + source_tesim + abstract_tesim + description_tesim + note_tesim + subject_label_tesim + identifier_ssim + bibliographic_citation_tesim + english_language_date_teim + file_format_tesim + extracted_text_tsimv + keyword_tesim + date_associated_tesim + subject_ocm_tesim + inscription_tesim ] field.solr_parameters = { @@ -226,10 +243,11 @@ class CatalogController < ApplicationController config.add_sort_field 'title_sort_si asc', label: "Title \u25B2" config.add_sort_field 'title_sort_si desc', label: "Title \u25BC" - # If there are more than this many search results, no spelling ("did you - # mean") suggestion is offered. + # If there are more than this many search results, no spelling ("did you mean") suggestion is offered. config.spell_max = 5 + config.search_state_fields += [:verb] + # OAI-PMH provider params # (see: https://github.com/projectblacklight/blacklight_oai_provider#configuration) config.oai = { @@ -237,13 +255,14 @@ class CatalogController < ApplicationController repository_name: 'Lafayette Digital Repository', repository_url: 'https://ldr.lafayette.edu', record_prefix: 'oai:ldr', - admin_email: 'dss@lafayette.edu' + admin_email: 'repository@lafayette.edu' }, document: { set_model: Spot::OaiCollectionSolrSet, set_fields: [ - { label: 'collection', solr_field: 'member_of_collections_ssim' } + { label: 'collection_id', solr_field: 'member_of_collection_ids_ssim' } + # { label: 'collection', solr_field: 'member_of_collections_ssim' } ] } } diff --git a/app/controllers/concerns/spot/additional_formats_for_controller.rb b/app/controllers/concerns/spot/additional_formats_for_controller.rb index 25f3ccfea..ca2ef684e 100644 --- a/app/controllers/concerns/spot/additional_formats_for_controller.rb +++ b/app/controllers/concerns/spot/additional_formats_for_controller.rb @@ -22,7 +22,7 @@ def csv_filename # @return [String] def work_as_csv - Spot::WorkCSVService.new(presenter.solr_document).csv + Spot::WorkCsvService.new(presenter.solr_document).csv end end end diff --git a/app/controllers/concerns/spot/cas_entitlement_patch_rack.rb b/app/controllers/concerns/spot/cas_entitlement_patch_rack.rb new file mode 100644 index 000000000..eac70aaf5 --- /dev/null +++ b/app/controllers/concerns/spot/cas_entitlement_patch_rack.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +module Spot + # Only store entitlements related to us in the session to prevent a cookie overflow. + module CasEntitlementPatchRack + extend ActiveSupport::Concern + + # :nocov: + def store_session(request, user, ticket, extra_attrs = {}) + extra_attrs.select! { |key, _val| RackCAS.config.extra_attributes_filter.map(&:to_s).include?(key.to_s) } if RackCAS.config.extra_attributes_filter? + + if extra_attrs['eduPersonEntitlement'].present? + extra_attrs['eduPersonEntitlement'] = Array.wrap(extra_attrs['eduPersonEntitlement']).select do |val| + URI.parse(val).host == Spot::CasUserRolesService.entitlement_host + end + end + + request.session['cas'] = { 'user' => user, 'ticket' => ticket, 'extra_attributes' => extra_attrs } + end + end +end diff --git a/app/controllers/concerns/spot/downloads_controller_behavior.rb b/app/controllers/concerns/spot/downloads_controller_behavior.rb index fbd38fc64..7cdab8811 100644 --- a/app/controllers/concerns/spot/downloads_controller_behavior.rb +++ b/app/controllers/concerns/spot/downloads_controller_behavior.rb @@ -8,6 +8,8 @@ module Spot # Hyrax::DownloadsController.prepend(Spot::DownloadsControllerBehavior) # module DownloadsControllerBehavior + extend ActiveSupport::Concern + # Testing handled in spec/features/download_transcripts_spec.rb # # Add support to download a FileSet's attached :transcript file. @@ -27,8 +29,7 @@ module DownloadsControllerBehavior # :nocov: def load_file return super unless params[:file] == 'transcript' - pcdm_file = dereference_file(params[:file]).find_target - ActiveFedora::File.find(pcdm_file.id) if pcdm_file&.id + dereference_file(params[:file]).reader end # :nocov: end diff --git a/app/controllers/concerns/spot/one_column_layout.rb b/app/controllers/concerns/spot/one_column_layout.rb new file mode 100644 index 000000000..02e6318e2 --- /dev/null +++ b/app/controllers/concerns/spot/one_column_layout.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +module Spot + # Mixin patch for Hyrax controllers that we'd prefer to render as 'hyrax/1_column' + # + # @example in initializer + # Rails.application.reloader.to_prepare do + # Hyrax::PagesController.prepend(Spot::OneColumnLayout) + # end + # + # @see config/initializers/spot_overrides.rb + module OneColumnLayout + extend ActiveSupport::Concern + + included do + layout 'hyrax/1_column' + end + end +end diff --git a/app/controllers/concerns/spot/updated_collections_dashboard_controller_facets_behavior.rb b/app/controllers/concerns/spot/updated_collections_dashboard_controller_facets_behavior.rb new file mode 100644 index 000000000..3a2aaa8a6 --- /dev/null +++ b/app/controllers/concerns/spot/updated_collections_dashboard_controller_facets_behavior.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +# +# the dashboard/my/collections (+ thus, dashboard/collections) controller defines +# blacklight facets + uses I18n.t to provide a label. as we've found from past experience, +# this can get called _before_ all of the locales are loaded, resulting in a +# "translation missing" message being provided as a fall-back label. this should +# prevent that error from appearing by replacing the +translate+ calls with a symbolized +# I18n key (see also 0717dee, + catalog_controller.rb) +module Spot + module UpdatedCollectionsDashboardControllerFacetsBehavior + extend ::ActiveSupport::Concern + + module ClassMethods + def update_facet_labels! + blacklight_config.facet_fields['visibility_ssi'].label = :'hyrax.dashboard.my.heading.visibility' + blacklight_config.facet_fields[Hyrax.config.collection_type_index_field].label = :'hyrax.dashboard.my.heading.collection_type' + blacklight_config.facet_fields['has_model_ssim'].label = :'hyrax.dashboard.my.heading.collection_type' + end + end + + included { self.class.update_facet_labels! } + end +end diff --git a/app/controllers/concerns/spot/updated_works_dashboard_controller_facets_behavior.rb b/app/controllers/concerns/spot/updated_works_dashboard_controller_facets_behavior.rb new file mode 100644 index 000000000..0fa1fd534 --- /dev/null +++ b/app/controllers/concerns/spot/updated_works_dashboard_controller_facets_behavior.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +# +# @see Spot::UpdatedCollectionsDashboardControllerFacetsBehavior +module Spot + module UpdatedWorksDashboardControllerFacetsBehavior + extend ::ActiveSupport::Concern + + module ClassMethods + def update_facet_labels! + blacklight_config.facet_fields['visibility_ssi'].label = :'hyrax.dashboard.my.heading.visibility' + end + end + + included { self.class.update_facet_labels! } + end +end diff --git a/app/controllers/concerns/spot/works_controller_behavior.rb b/app/controllers/concerns/spot/works_controller_behavior.rb index 1b8571f24..9179ae178 100644 --- a/app/controllers/concerns/spot/works_controller_behavior.rb +++ b/app/controllers/concerns/spot/works_controller_behavior.rb @@ -12,30 +12,66 @@ module WorksControllerBehavior extend ActiveSupport::Concern include ::Hyrax::WorksControllerBehavior include ::Hyrax::BreadcrumbsForWorks - include AdditionalFormatsForController included do before_action :load_workflow_presenter, only: :edit after_action :update_workflow_flash, only: :update + + self.work_form_service = Spot::WorkFormService end private + # @note valkyrie forms only accept the resource in the initializer, + # so our specs are currently failing when HYRAX_VALKYRIE=1. + # I'm assuming this is fixed upstream, so I'm just going to + # patch this for now. + # @todo Remove when Hyrax > 5 + # def build_form + # super() + # rescue ArgumentError + # @form = work_form_service.form_class(curation_concern).new(curation_concern) + # end + + def additional_response_formats(wants) + super + + wants.csv do + content = Spot::WorkCsvService.new(presenter.solr_document).csv + send_data(content, type: 'text/csv', filename: "#{presenter.id}.csv") + end + end + # Overrides Hyrax behavior by using our own IIIF presenter that relies on Blacklight locales # to generate field labels. # # @return [Spot::IiifManifestPresenter] + # @todo is there a way we can add test coverage for this? + # + # :nocov: def iiif_manifest_presenter - ::Spot::IiifManifestPresenter.new(curation_concern_from_search_results).tap do |p| + ::Spot::IiifManifestPresenter.new(search_result_document(id: params[:id], defType: 'lucene')).tap do |p| p.hostname = request.hostname p.ability = current_ability end end + # :nocov: def load_workflow_presenter @workflow_presenter = Hyrax::WorkflowPresenter.new(::SolrDocument.find(params[:id]), current_ability) end + # @see https://github.com/samvera/hyrax/blob/hyrax-v3.6.0/app/controllers/concerns/hyrax/works_controller_behavior.rb#L252-L253 + # @todo is there a way we can add test coverage for this? + # + # :nocov: + def search_params + params.deep_dup.tap do |sp| + sp.delete(:page) + end + end + # :nocov: + # When the workflow presenter has actions available, append a note to the update flash that the # review form needs to be marked as completed. # diff --git a/app/controllers/handle_controller.rb b/app/controllers/handle_controller.rb index e43aca06a..3b791b445 100644 --- a/app/controllers/handle_controller.rb +++ b/app/controllers/handle_controller.rb @@ -6,15 +6,13 @@ class HandleController < ApplicationController include ::Spot::RedirectionHelpers # Searches for a Handle based on an +hdl:+ identifier. - # Displays a 404 (via raised +Blacklight::Exceptions::RecordNotFound+ - # that is handled with +Hydra::Catalog+) if no item is found. + # Displays a 404 (via raised +Hyrax::ObjectNotFoundError+) if no item is found. def show - query = query_for_identifier(Spot::Identifier.new('hdl', params[:id])) - result, _documents = repository.search(query) - - raise Blacklight::Exceptions::RecordNotFound if result.response['numFound'].zero? - document = result.response['docs'].first + query_opts = query_for_identifier(Spot::Identifier.new('hdl', params[:id])) + service = Hyrax::SolrQueryService.new(query: [query_opts.delete(:q)]) + document = service.get(**query_opts)['response']['docs'].first + raise Blacklight::Exceptions::RecordNotFound if document.nil? redirect_to redirect_params_for(solr_document: document) end @@ -32,7 +30,6 @@ def identifier_solr_field # @param id [Spot::Identifier, #to_s] the identifier (with prefix) # @return [Hash String>] def query_for_identifier(id) - { q: "{!terms f=#{identifier_solr_field}}#{id}", - defType: 'lucene' } + { q: "{!terms f=identifier_ssim}#{id}", defType: 'lucene' } end end diff --git a/app/controllers/hyrax/audio_visuals_controller.rb b/app/controllers/hyrax/audio_visuals_controller.rb index 94f74f22c..61e7dd227 100644 --- a/app/controllers/hyrax/audio_visuals_controller.rb +++ b/app/controllers/hyrax/audio_visuals_controller.rb @@ -4,8 +4,8 @@ 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.curation_concern_type = ::AudioVisual 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..edde0c7a1 100644 --- a/app/controllers/hyrax/images_controller.rb +++ b/app/controllers/hyrax/images_controller.rb @@ -3,7 +3,7 @@ module Hyrax class ImagesController < ApplicationController include ::Spot::WorksControllerBehavior - self.curation_concern_type = ::Image + self.curation_concern_type = Image 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..b8603cd27 100644 --- a/app/controllers/hyrax/publications_controller.rb +++ b/app/controllers/hyrax/publications_controller.rb @@ -3,7 +3,7 @@ module Hyrax class PublicationsController < ApplicationController include Spot::WorksControllerBehavior - self.curation_concern_type = ::Publication + self.curation_concern_type = Publication 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..3e88cd2d8 100644 --- a/app/controllers/hyrax/student_works_controller.rb +++ b/app/controllers/hyrax/student_works_controller.rb @@ -3,7 +3,7 @@ module Hyrax class StudentWorksController < ApplicationController include Spot::WorksControllerBehavior - self.curation_concern_type = ::StudentWork + self.curation_concern_type = StudentWork self.show_presenter = Hyrax::StudentWorkPresenter # Modifying the search_builder_class to our subclass which allows diff --git a/app/controllers/spot/export_controller.rb b/app/controllers/spot/export_controller.rb index 1c9709f9e..fa6e2ab3e 100644 --- a/app/controllers/spot/export_controller.rb +++ b/app/controllers/spot/export_controller.rb @@ -52,7 +52,7 @@ def export_work_to_cache! # @return [Spot::Exporters::ZippedWorkExporter] def exporter - Spot::Exporters::ZippedWorkExporter.new(solr_document, request) + Spot::Exporters::ZippedWorkExporter.new(solr_document) end # Sets the @solr_document attribute from a single-item solr query. diff --git a/app/controllers/spot/redirect_controller.rb b/app/controllers/spot/redirect_controller.rb index 08c293ed9..4b373d4e3 100644 --- a/app/controllers/spot/redirect_controller.rb +++ b/app/controllers/spot/redirect_controller.rb @@ -28,9 +28,8 @@ def show private def document_params - # The only fields we'll need to generate a URL (or url_helper params) is are id and has_model_ssim. - result, _documents = repository.search(q: "{!terms f=identifier_ssim}url:#{http_uri}", fl: ['id', 'has_model_ssim'], defType: 'lucene') - document = result.response['docs']&.first + service = Hyrax::SolrQueryService.new(query: ["{!terms f=identifier_ssim}url:#{http_uri}"]) + document = service.query_result(fl: ['id', 'has_model_ssim'], defType: 'lucene')['response']['docs'].first raise Blacklight::Exceptions::RecordNotFound if document.nil? redirect_params_for(solr_document: document) diff --git a/app/forms/audio_visual_resource_form.rb b/app/forms/audio_visual_resource_form.rb new file mode 100644 index 000000000..e1723bb57 --- /dev/null +++ b/app/forms/audio_visual_resource_form.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true +# +# Form to edit AudioVisualResource objects +class AudioVisualResourceForm < ::Hyrax::Forms::ResourceForm(AudioVisualResource) + include Spot::Forms::ResourceFormBehavior + + include Hyrax::FormFields(:base_metadata) + include Hyrax::FormFields(:audio_visual_metadata) + + include Spot::Forms::LanguageTaggedFormFields.for(:title, :title_alternative, :subtitle, :description, :inscription) + include Spot::Forms::ControlledVocabularyFormField.for(:language) + include Spot::Forms::IdentifierFormFields + + validates_with Spot::EdtfDateValidator, fields: [:date] + + # rubocop:disable Metrics/MethodLength + def primary_terms + [ + # required_fields first + :title, + :date, + :resource_type, + :rights_statement, + + # non-required fields + :rights_holder, + :subtitle, + :title_alternative, + :date_associated, + :creator, + :contributor, + :publisher, + :source, + :standard_identifier, + :local_identifier, + :description, + :inscription, + :subject, + :keyword, + :language, + :physical_medium, + :original_item_extent, + :location, + :repository_location, + :note, + :related_resource, + :research_assistance, + :provenance, + :barcode + ] + end + # rubocop:enable Metrics/MethodLength +end diff --git a/app/forms/concerns/language_tagged_form_fields.rb b/app/forms/concerns/language_tagged_form_fields.rb index 5ea67eb12..e806c1503 100644 --- a/app/forms/concerns/language_tagged_form_fields.rb +++ b/app/forms/concerns/language_tagged_form_fields.rb @@ -106,7 +106,7 @@ def map_rdf_strings(tuples) # @return [RdfLiteralSerializer] def serializer - @serializer ||= RdfLiteralSerializer.new + RdfLiteralSerializer end end end diff --git a/app/forms/concerns/spot/forms/batch_edit_form_terms_and_permitted_params.rb b/app/forms/concerns/spot/forms/batch_edit_form_terms_and_permitted_params.rb new file mode 100644 index 000000000..de91ef968 --- /dev/null +++ b/app/forms/concerns/spot/forms/batch_edit_form_terms_and_permitted_params.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +# +# Mixin to modify Hyrax's BatchEditForm fields to add our own common metadata fields +# to the form. Originally written as a class_eval in spot_overrides, this Module is now +# intended to be included in the spot_overrides file like: +# +# @example +# Rails.application.reloader.to_prepare do +# # ... +# Hyrax::Forms::BatchEditForm.include(Spot::Forms::BatchEditFormTermsAndPermittedParams) +# end +# +# @note This is used for the pre-Valkyrization forms and can be removed once we're fully Valkyrized. +# @see config/initializers/spot_overrides.rb +module Spot + module Forms + module BatchEditFormTermsAndPermittedParams + extend ActiveSupport::Concern + + included do + self.terms = [ + :creator, :contributor, :description, :note, + :keyword, :resource_type, :license, :publisher, + :subject, :language, :identifier, :location, + :related_resource + ] + + def self.build_permitted_params + original_params = super + skip_keys = [:based_near, :based_near_attributes, :date_created] + + original_params.reject { |config| skip_keys.any? { |k| config.is_a?(Hash) ? config.key?(k) : config == k } } + .union([{ note: [] }, { location: [] }, { location_attributes: [:id, :_destroy] }]) + end + end + end + end +end diff --git a/app/forms/concerns/spot/forms/controlled_vocabulary_form_field.rb b/app/forms/concerns/spot/forms/controlled_vocabulary_form_field.rb new file mode 100644 index 000000000..05ff6a222 --- /dev/null +++ b/app/forms/concerns/spot/forms/controlled_vocabulary_form_field.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true +module Spot + module Forms + # Mixin to add support for Controlled Vocabulary fields in Resource forms. + # + # @usage + # class CoolNewResourceForm < Hyrax::Forms::ResourceForm(CoolNewResource) + # include Hyrax::FormFields(:base_metadata) + # include Hyrax::FormFields(:cool_new_resource_metadata) + # include Spot::Forms::ControlledVocabularyFormField.for(:academic_department) + # include Spot::Forms::ControlledVocabularyFormField.for(:subject, model_wrapper: Spot::ControlledVocabularies::AssignFastSubject) + # + # # ... + # end + # + # Similarly to `Spot::LanguageTaggedFormFields`, this creates a virtual `_attributes` property that + # is parsed downstream by `ControlledVocabularyInput` and its descendants into a Select2 typeahead input. + # For local controlled vocabularies (eg. Language, Academic Departments, Subject OCM) the values are simply + # cast as strings, but for remote authorities (eg. Subject, Location), we need to wrap URI values in an + # `ActiveTriples::Resource` class to fetch values. This was previously configured in ActiveFedora within the + # model's property via a `:class_name attribute, but for now we'll define it in the form instead of the model. + # + # @todo Maybe this behavior _should_ be defined within the model? Not sure how you'd cast an empty value in the + # form, though. Maybe configured in two places? (redundancy?) + class ControlledVocabularyFormField < Module + # @param [Symbol] field + # @option [Class] :model_wrapper + # @return [Spot::Forms::ControlledVocabularyFormField] + def self.for(field, model_wrapper: String) + new(field: field, model_wrapper: model_wrapper) + end + + def initialize(field:, model_wrapper:) + @field = field + @model_wrapper = model_wrapper + end + + private + + def included(descendant) + super + + descendant.include(HelperMethods) + + field = @field.to_s + attributes_key = "#{field}_attributes".to_sym + prepopulator_key = "#{field}_prepopulator".to_sym + populator_key = "#{field}_populator".to_sym + + descendant.define_method(prepopulator_key) do + vocab_class = @model_wrapper || String + send(:"#{attributes_key}=", wrap_attribute_values(field: field, field_value_class: vocab_class)) + end + + descendant.define_method(populator_key) do |fragment:, **| + send(:"#{field}=", parse_attribute_fragment(fragment: fragment, field: field)) + end + + descendant.property(attributes_key, virtual: true, prepopulator: prepopulator_key, populator: populator_key) + end + + module HelperMethods + # Called from within the _attributes :populator method to parse through + # an incoming hash and return the intended field's values — original + # form values concated with incoming _attributes additions (see @note below) + # + # @params [Hash] options + # @option [Hash] fragment (see above for incoming hash format) + # @option [String] field + # @return [Array] + # + # @note I don't believe we need to cast the values that are returned, I + # think the Resource model will take care of that? + # + # @note in theory we're setting the _attributes field with the original + # form values on prepopulation, so we shouldn't need to including + # them in the output, but this is following Hyrax conventions in + # Hyrax::Forms::PcdmObjectForm. + # + # @see https://github.com/samvera/hyrax/blob/hyrax-v3.6.0/app/forms/hyrax/forms/pcdm_object_form.rb#L31-L43 + def parse_attribute_fragment(fragment:, field:, value_key: 'id') + adds = [] + deletes = [] + + fragment.each do |_idx, attrs| + value = attrs[value_key] + if attrs['_destroy'] == 'true' + deletes << value + else + adds << value + end + end + + ((Array.wrap(send(field)).map(&:to_s) + adds) - deletes).uniq + end + + # @option [Symbol,String] field + # @option [String] value_key (default: 'id') + # @return [Hash Hash String>>] + def wrap_attribute_values(field:, field_value_class: String) + values = Array.wrap(send(field)).map { |v| v.is_a?(field_value_class) ? v : field_value_class.new(v) } + values << field_value_class.new if values.empty? + values + end + + # Valkyrie::ChangeSet includes Reform::Form::ActiveModel::FormBuilderMethods + # which has behavior that will mutate incoming form params so that *_attributes + # values are copied to their respective field. Since we're adding our own handling + # for *_attributes _after_ the initial form fields are being defined, calls to + # `send(field)` will result in _attributes' value. By making this method + # a no-op, we prevent this tomfoolery. + # + # @see https://github.com/trailblazer/reform-rails/blob/v0.2.5/lib/reform/form/active_model/form_builder_methods.rb#L37-L46 + def rename_nested_param_for!(_params, _dfn); end + end + end + end +end diff --git a/app/forms/concerns/spot/forms/identifier_form_fields.rb b/app/forms/concerns/spot/forms/identifier_form_fields.rb new file mode 100644 index 000000000..f0680bb06 --- /dev/null +++ b/app/forms/concerns/spot/forms/identifier_form_fields.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true +module Spot + module Forms + # Mixin to add support for separate standard/local identifier form fields. We store all identifiers + # in a single field (typically :identifier) but include prefixes to determine the origin of the value. + # In the form, we want to split out "standard" identifiers (registered in the Spot::Identifier initializer) + # with known prefixes from "local" identifiers, which are ad-hoc. This takes care of that splitting and + # merging work in the form context. + # + # @example + # class WorkResourceForm < Hyrax::Form(WorkResource) + # include Hyrax::FormFields(:work_resource) + # include Spot::Forms::IdentifierFormFields + # end + # + # + # @example To change which field this points to, update the :identifier_field class attribute + # class WorkResourceForm < Hyrax::Form(WorkResource) + # include Hyrax::FormFields(:work_resource) + # include Spot::Forms::IdentifierFormFields + # + # self.identifier_field = :external_identifier + # end + # + module IdentifierFormFields + extend ActiveSupport::Concern + + included do + class_attribute :identifier_field, default: :identifier + + property :local_identifier, + virtual: true, + display: true, + prepopulator: -> { self.local_identifier = local_identifiers.map(&:to_s) } + + property :standard_identifier_prefix, + virtual: true, + display: true, + prepopulator: -> { self.standard_identifier_prefix = standard_identifiers.map(&:prefix) } + property :standard_identifier_value, + virtual: true, + display: true, + prepopulator: -> { self.standard_identifier_value = standard_identifiers.map(&:value) } + + validate(identifier_field) do + send(:"#{identifier_field}=", merged_identifiers) + end + end + + # We store the work's NOID as an identifier, but don't want to allow it to be edited. + def local_identifiers + wrapped_identifiers.select(&:local?).reject { |id| id.prefix == 'noid' } + end + + # @todo why am I aliasing this again? Anna-convenience or some other nefarious reason? + alias local_identifier local_identifiers + + def merged_identifiers + [ + send(identifier_field), + merged_standard_identifiers, + local_identifier + ].flatten.compact.uniq + end + + def merged_standard_identifiers + prefixes = Array.wrap(standard_identifier_prefix) + values = Array.wrap(standard_identifier_value) + prefixes.zip(values) + .flat_map { |(prefix, id)| Spot::Identifier.new(prefix, id).to_s } + .uniq + end + + def standard_identifiers + wrapped_identifiers.select(&:standard?) + end + alias standard_identifier standard_identifiers + + def wrapped_identifiers + Array.wrap(send(identifier_field)).map { |id| Spot::Identifier.from_string(id) } + end + end + end +end diff --git a/app/forms/concerns/spot/forms/language_tagged_form_fields.rb b/app/forms/concerns/spot/forms/language_tagged_form_fields.rb new file mode 100644 index 000000000..c568727eb --- /dev/null +++ b/app/forms/concerns/spot/forms/language_tagged_form_fields.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true +module Spot + module Forms + # Adds support for adding a language tag to a value within the form. This is done + # by creating virtual "_value" and "_language" fields in the form, parsed from RDF::Literals, + # and merging them when the form is submitted. Include using the method invocation and pass + # the field names. + # + # @example + # class WorkResourceForm < ::Hyrax::Forms::ResourceForm(WorkResource) + # include Hyrax::FormFields(:metadata_schema) + # include Spot::Forms::LanguageTaggedFormFields.for(:title, :title_alternative) + # end + class LanguageTaggedFormFields < Module + def self.for(*fields) + new(fields: fields.flatten) + end + + def initialize(fields:) + @fields = fields + end + + private + + def language_prepopulator_for(field:) + lambda do + vals = Array.wrap(send(field.to_sym)).map do |original| + case original + when RDF::Literal + original.language.to_s + end + end.compact + + send(:"#{field}_language=", self.class.definitions[field.to_s][:multiple] ? vals : vals.first) + end + end + + def value_prepopulator_for(field:) + lambda do + vals = Array.wrap(send(field.to_sym)).map do |original| + case original + when RDF::Literal + original.value.to_s + else + original + end + end.compact + + send(:"#{field}_value=", self.class.definitions[field.to_s][:multiple] ? vals : vals.first) + end + end + + def value_populator_for(field:) + lambda do |doc:, **| + vals = Array.wrap(doc["#{field}_value"]).zip(Array.wrap(doc["#{field}_language"])).map do |(value, language)| + if value.present? && language.present? + RDF::Literal.new(value.to_s, language: language.to_sym) + elsif value.present? + RDF::Literal.new(value.to_s) + end + end.compact + + vals = vals.first unless self.class.definitions[field.to_s][:multiple] + + send(:"#{field}=", vals) + end + end + + def included(descendant) + super + + @fields.map(&:to_sym).each do |field| + default_value = descendant.definitions[field.to_s][:default].call + descendant.property(:"#{field}_value", + virtual: true, + default: default_value, + prepopulator: value_prepopulator_for(field: field), + populator: value_populator_for(field: field)) + descendant.property(:"#{field}_language", + virtual: true, + default: default_value, + prepopulator: language_prepopulator_for(field: field)) + + descendant.validates(field, presence: true) if descendant.definitions[field.to_s][:required] + end + end + end + end +end diff --git a/app/forms/concerns/spot/forms/resource_form_behavior.rb b/app/forms/concerns/spot/forms/resource_form_behavior.rb new file mode 100644 index 000000000..337715c21 --- /dev/null +++ b/app/forms/concerns/spot/forms/resource_form_behavior.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +module Spot + # Mixin for common form behaviors (in place of a BaseForm object to inherit from) + module Forms + module ResourceFormBehavior + extend ActiveSupport::Concern + + # Hyrax form behavior is to display 'primary' terms above the fold and an 'additional fields' + # button to expand the rest of the fields. We only use this behavior for non-staff interfaces, + # and display all of the terms by default. + # + # @return [Array] + def primary_terms + _form_field_definitions.select { |_, definition| definition[:display] }.keys.map(&:to_sym) + end + + # By default, keep the 'below the fold' fields empty. + # + # @return [Array] + def secondary_terms + [] + end + end + end +end diff --git a/app/forms/hyrax/forms/batch_edit_form.rb b/app/forms/hyrax/forms/batch_edit_form.rb deleted file mode 100644 index 3c2c56f1b..000000000 --- a/app/forms/hyrax/forms/batch_edit_form.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true -# -# Copied this from Hyrax @ 780c148. We're not using the BasicMetadata -# mixin in our work models, and this form is expecting those fields to -# be present, which causes some gross errors on the UI side. I'll try -# to keep track of the customizations here. -# -# - remove :date_created -# - change :based_near => :location -# - change :related_url => :related_resource -# - add :note -module Hyrax - module Forms - class BatchEditForm < Hyrax::Forms::WorkForm - # Used for drawing the fields that appear on the page - self.terms = [:creator, :contributor, :description, :note, - :keyword, :resource_type, :license, :publisher, - :subject, :language, :identifier, :location, - :related_resource] - self.required_fields = [] - self.model_class = Hyrax.primary_work_type - - # Contains a list of titles of all the works in the batch - attr_accessor :names - - # @param [ActiveFedora::Base] model the model backing the form - # @param [Ability] current_ability the user authorization model - # @param [Array] batch_document_ids a list of document ids in the batch - def initialize(model, current_ability, batch_document_ids) - @names = [] - @batch_document_ids = batch_document_ids - @combined_attributes = initialize_combined_fields - super(model, current_ability, nil) - end - - attr_reader :batch_document_ids - - # Returns a list of parameters we accept from the form - def self.build_permitted_params - [{ creator: [] }, - { contributor: [] }, - { description: [] }, - { note: [] }, - { keyword: [] }, - { resource_type: [] }, - { license: [] }, - { publisher: [] }, - { subject: [] }, - { language: [] }, - { identifier: [] }, - { location: [] }, - { related_resource: [] }, - { permissions_attributes: [:type, :name, :access, :id, :_destroy] }, - :on_behalf_of, - :version, - :add_works_to_collection, - :visibility_during_embargo, - :embargo_release_date, - :visibility_after_embargo, - :visibility_during_lease, - :lease_expiration_date, - :visibility_after_lease, - :visibility, - { location_attributes: [:id, :_destroy] }] - end - - private - - attr_reader :combined_attributes - - # override this method if you need to initialize more complex RDF assertions (b-nodes) - # @return [Hash] the list of unique values per field - def initialize_combined_fields - # For each of the files in the batch, set the attributes to be the concatenation of all the attributes - batch_document_ids.each_with_object({}) do |doc_id, combined_attributes| - work = ActiveFedora::Base.find(doc_id) - terms.each do |field| - combined_attributes[field] ||= [] - combined_attributes[field] = (combined_attributes[field] + work[field].to_a).uniq - end - names << work.to_s - end - end - - def initialize_field(key) - # if value is empty, we create an one element array to loop over for output - return model[key] = combined_attributes[key] if combined_attributes[key].present? - super - 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..b0a2f718f --- /dev/null +++ b/app/forms/image_resource_form.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true +# +# Form to edit ImageResource objects, added for use with Hyrax's Valkyrie adapter. +# +# @note need to :save_constantize Hyrax's ResourceForm so that the class is loaded +# + Rails knows about their new constant-method approach (otherwise this will +# raise a NoMethodError for :ResourceForm on the Hyrax::Forms module) +# +# @example Creating a form for a new resource (Hyrax v4.0) +# form = Hyrax::Forms::ResourceForm.for(ImageResource.new) +# +# @example Creating a form for a new resource (Hyrax >= v5.0) +# form = Hyrax::Forms::ResourceForm.for(resource: ImageResource.new) +# +# @example Creating a form for an existing resource (Hyrax v4.0) +# resource = Hyrax.query_service.find_by_alternate_identifier(alternate_identifier: 'abc123def') +# form = Hyrax::Forms::ResourceForm.for(resource) +# +# @example Creating a form for an existing resource (hyrax >= v5.0) +# resource = Hyrax.query_service.find_by_alternate_identifier(alternate_identifier: 'abc123def') +# form = Hyrax::Forms::ResourceForm.for(resource: resource) +# +'Hyrax::Forms::ResourceForm'.safe_constantize + +class ImageResourceForm < ::Hyrax::Forms::ResourceForm(ImageResource) + include Spot::Forms::ResourceFormBehavior + + include Hyrax::FormFields(:base_metadata) + include Hyrax::FormFields(:image_metadata) + include Spot::Forms::IdentifierFormFields + + include Spot::Forms::LanguageTaggedFormFields.for(:title, :title_alternative, :subtitle, :description, :inscription) + include Spot::Forms::ControlledVocabularyFormField.for(:language) + include Spot::Forms::ControlledVocabularyFormField.for(:subject_ocm) + include Spot::Forms::ControlledVocabularyFormField.for(:location, model_wrapper: Spot::ControlledVocabularies::Location) + include Spot::Forms::ControlledVocabularyFormField.for(:subject, model_wrapper: Spot::ControlledVocabularies::AssignFastSubject) + + validates_with Spot::EdtfDateValidator, fields: [:date] + + # Define the order in which we display fields for the user. These are rendered + # "above the fold," or unobscurred by a hideaway toggle. + # + # @return [Array] + # @todo Should we move repository-centric fields to #secondary_terms or does it not really + # matter for a staff-side object? + # + # rubocop:disable Metrics/MethodLength + def primary_terms + [ + :title, + :date, + :resource_type, + :rights_statement, + + # non-required fields + :title_alternative, + :subtitle, + :date_associated, + :date_scope_note, + :rights_holder, + :description, + :inscription, + :creator, + :contributor, + :publisher, + :keyword, + :subject, + :location, + :language, + :source, + :physical_medium, + :original_item_extent, + :repository_location, + :requested_by, + :research_assistance, + :donor, + :related_resource, + :local_identifier, + :subject_ocm, + :note + ] + end + # rubocop:enable Metrics/MethodLength +end diff --git a/app/forms/publication_resource_form.rb b/app/forms/publication_resource_form.rb new file mode 100644 index 000000000..b5b943819 --- /dev/null +++ b/app/forms/publication_resource_form.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true +'Hyrax::Forms::ResourceForm'.safe_constantize + +class PublicationResourceForm < ::Hyrax::Forms::ResourceForm(PublicationResource) + include Spot::Forms::ResourceFormBehavior + + include Hyrax::FormFields(:base_metadata) + include Hyrax::FormFields(:institutional_metadata) + include Hyrax::FormFields(:publication_metadata) + + include Spot::Forms::LanguageTaggedFormFields.for(:title, :title_alternative, :subtitle, :abstract, :description) + include Spot::Forms::IdentifierFormFields + + include Spot::Forms::ControlledVocabularyFormField.for(:location, model_wrapper: Spot::ControlledVocabularies::Location) + include Spot::Forms::ControlledVocabularyFormField.for(:subject, model_wrapper: Spot::ControlledVocabularies::AssignFastSubject) + include Spot::Forms::ControlledVocabularyFormField.for(:academic_department) + include Spot::Forms::ControlledVocabularyFormField.for(:division) + include Spot::Forms::ControlledVocabularyFormField.for(:language) + + validates_with Spot::EdtfDateValidator, fields: [:date_issued] + + # Set a date_available value to either the embargo's release date (where present) + # or the current day, in YYYY-MM-DD format. + validate(:date_available) do + next if date_available.present? + + self.date_available = + if embargo_release_date.present? + date_time = embargo_release_date.respond_to?(:strftime) ? embargo_release_date : DateTime.parse(embargo_release_date.to_s) + [date_time.strftime('%Y-%m-%d')] + else + [DateTime.now.strftime('%Y-%m-%d')] + end + end + + def primary_terms + [ + # required_fields first + :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, + :note, + :related_resource, + :academic_department, + :division, + :organization + ] + end +end diff --git a/app/forms/spot/forms/collection_form.rb b/app/forms/spot/forms/collection_form.rb index 2da5ec611..0cb47e7a4 100644 --- a/app/forms/spot/forms/collection_form.rb +++ b/app/forms/spot/forms/collection_form.rb @@ -25,6 +25,8 @@ class CollectionForm < Hyrax::Forms::CollectionForm include ::SingularFormFields include ::StripsWhitespace + self.model_class = ::Collection + transforms_language_tags_for :title, :abstract, :description transforms_nested_fields_for :language diff --git a/app/forms/student_work_resource_form.rb b/app/forms/student_work_resource_form.rb new file mode 100644 index 000000000..9a6408f39 --- /dev/null +++ b/app/forms/student_work_resource_form.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true +# +# Form to edit StudentWorkResource objects +# +# @todo In StudentWorkForm we default :creator and :rights_holder to the name +# of the depositing user, but I don't think we have a hook in the form. +# This might need to be added at the controller level. +# +# @todo Update 2025-06-23: we can do this by adding a prepopulator for those +# fields and evaluating the ability by instantiating a User using the +# `resource.depositor` email (which is added to the resource before +# a new form is created). Defining the prepopulator might be as simple as +# defining a `:_prepopulator` method + maybe overriding the field configuration +# similarly to how we're creating single fields for students (which itself needs +# to be revisited) +# @see https://github.com/samvera/hyrax/blob/hyrax-v4.0.0/app/controllers/concerns/hyrax/works_controller_behavior.rb#L57 +'Hyrax::Forms::ResourceForm'.safe_constantize + +class StudentWorkResourceForm < ::Hyrax::Forms::ResourceForm(StudentWorkResource) + DEFAULT_RIGHTS_STATEMENT_URI = 'http://rightsstatements.org/vocab/InC-EDU/1.0/' + + include Spot::Forms::ResourceFormBehavior + + include Hyrax::FormFields(:base_metadata) + include Hyrax::FormFields(:institutional_metadata) + include Hyrax::FormFields(:student_work_metadata) + + include Spot::Forms::ControlledVocabularyFormField.for(:location, model_wrapper: Spot::ControlledVocabularies::Location) + include Spot::Forms::ControlledVocabularyFormField.for(:subject, model_wrapper: Spot::ControlledVocabularies::AssignFastSubject) + include Spot::Forms::ControlledVocabularyFormField.for(:academic_department) + include Spot::Forms::ControlledVocabularyFormField.for(:advisor) + include Spot::Forms::ControlledVocabularyFormField.for(:division) + include Spot::Forms::ControlledVocabularyFormField.for(:language) + + include Spot::Forms::IdentifierFormFields + + validates_with Spot::EdtfDateValidator, fields: [:date] + + # Hack to allow us to override certain form fields to make them singular when they're multiple + # in other places. We were able to do this in earlier Hyrax forms by evaluating the user accessing + # the form, but I think that behavior is decoupled in the Valkyrized world and performed during + # the change_set saving transaction (iirc). + %w[abstract date date_available description].each do |field| + definitions[field].merge!(multiple: false, default: proc { nil }) + end + + # @todo provide the StudentWork admin_set as a default? Or stuff the value and not expose it? + # def admin_set_id; end + + 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 + + def description + Array.wrap(super).first + end + + def rights_statement + super || [DEFAULT_RIGHTS_STATEMENT_URI] + end +end diff --git a/app/helpers/audio_visual_helper.rb b/app/helpers/audio_visual_helper.rb index 2c7fb9cb6..5adb14446 100644 --- a/app/helpers/audio_visual_helper.rb +++ b/app/helpers/audio_visual_helper.rb @@ -28,7 +28,7 @@ def s3_url(key) end # Matches fileset ids with their presenters and returns their original file names for audio playlist - # @param presntres [[FileSetPresenter]] list of file set presenters attactched to a work + # @param presenters [Array] list of file set presenters attactched to a work # @param derivative [String] a particular derivative key to be matched with a presenter # @return [String] the original file name of the given derivative def get_original_name(presenters, derivative) @@ -41,7 +41,7 @@ def get_original_name(presenters, derivative) # @param file_set [FileSet] a fileset from the view # @return [String] a list of associated derivatives of the work def get_derivative_list(presenters) - presenters.flat_map(&:stored_derivatives) + presenters.reduce([]) { |enum, presenter| enum + presenter.stored_derivatives.to_a } end # @param derivative [String] a particular derivative key diff --git a/app/helpers/hyrax_helper.rb b/app/helpers/hyrax_helper.rb index c5c6c14a6..784887b00 100644 --- a/app/helpers/hyrax_helper.rb +++ b/app/helpers/hyrax_helper.rb @@ -4,6 +4,15 @@ module HyraxHelper include Hyrax::BlacklightOverride include Hyrax::HyraxHelperBehavior + # We currently don't offer translations beyond those offered in Hyrax, so we'll default + # to English for now. This will hide a translation dropdown in the site's header. + # + # @see https://github.com/samvera/hyrax/blob/hyrax-v4.0.0/app/views/_user_util_links.html.erb + # @see https://github.com/samvera/hyrax/blob/hyrax-v4.0.0/app/helpers/hyrax/hyrax_helper_behavior.rb#L31-L43 + def available_translations + { 'en' => 'English' } + end + # @see https://github.com/samvera/hyrax/blob/v2.9.6/app/helpers/hyrax/title_helper.rb#L18-L22 def default_page_title i18n_key = "spot.#{controller_name.underscore}" diff --git a/app/helpers/pdf_viewer_helper.rb b/app/helpers/pdf_viewer_helper.rb index 4717007eb..054f6864d 100644 --- a/app/helpers/pdf_viewer_helper.rb +++ b/app/helpers/pdf_viewer_helper.rb @@ -3,6 +3,16 @@ # Helper methods to generate the PDFjs viewer and populate the search query # (if present) module PdfViewerHelper + # Whether or not to display a PDF file in the viewer + # + # @param [#file_size] file_set to test + # @param [Number] limit_mb size limit in megabytes + # @return [true,false] + def pdf_too_large_for_viewer?(file_set, limit_mb = 65) + file_size_in_mb = file_set.file_size.to_i / (1024 * 1024) + file_size_in_mb >= limit_mb + end + # @param [String] path # @return [String] URL to the viewer def viewer_url(path) diff --git a/app/helpers/spot/workflow_mailer_helper.rb b/app/helpers/spot/workflow_mailer_helper.rb index cc872226c..09de70966 100644 --- a/app/helpers/spot/workflow_mailer_helper.rb +++ b/app/helpers/spot/workflow_mailer_helper.rb @@ -34,13 +34,8 @@ def submission_has_comment? @comment.present? end - # The title of the Workflow Actions form widget. Hardcoded in Hyrax < 3 to "Review and Approval" - # but uses I18n.t beyond that. - # - # @todo update after Hyrax v3 upgrade def workflow_actions_title - "Review and Approval" - # I18n.t('hyrax.base.workflow_actions.title') + I18n.t('hyrax.base.workflow_actions.title') end 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..5c4c77b82 --- /dev/null +++ b/app/indexers/audio_visual_resource_indexer.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +class AudioVisualResourceIndexer < BaseResourceIndexer + include Hyrax::Indexer(:audio_visual_metadata) + + self.sortable_date_property = :date +end diff --git a/app/indexers/base_indexer.rb b/app/indexers/base_indexer.rb index 43a1bd9df..77f59634e 100644 --- a/app/indexers/base_indexer.rb +++ b/app/indexers/base_indexer.rb @@ -26,6 +26,7 @@ def generate_solr_document solr_doc['file_format_ssim'] = object.file_sets.map(&:mime_type).reject(&:blank?) store_thumbnail_url(solr_doc) + stringify_rdf_uris(solr_doc) end end @@ -48,4 +49,25 @@ def store_thumbnail_url(doc) doc['thumbnail_url_ss'] = url unless url.empty? end + + # @see app/indexers/base_resource_indexer.rb + def stringify_rdf_uris(document) + document.transform_values! do |values| + stringified_values = stringify_values(values) + values.is_a?(Array) ? stringified_values : stringified_values.first + end + end + + def stringify_values(values) + Array.wrap(values).map do |value| + case value + when RDF::Literal, RDF::URI + value.to_s + when ActiveTriples::Resource + value.rdf_subject + else + value + end + end + end end diff --git a/app/indexers/base_resource_indexer.rb b/app/indexers/base_resource_indexer.rb new file mode 100644 index 000000000..bcb7292da --- /dev/null +++ b/app/indexers/base_resource_indexer.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true +# +# Indexer used as a base for all of our Resource objects. Extend the #to_solr +# method using +super.tap+ to define fields specific to the resource. +# +# Subclasses should define the +sortable_date_property+ attribute to index +# a sortable date (resources have different primary date fields). +# +# @example +# class CoolResourceIndexer < BaseResourceIndexer +# include Hyrax::Indexer(:cool_metadata) +# self.sortable_date_property = :date_issued +# +# def to_solr +# super.tap do |document| +# document['some_metadata_field_ssim'] = resource.try(:metadata_field) +# end +# end +# end +# +class BaseResourceIndexer < ::Hyrax::ValkyrieWorkIndexer + # @note :core_metadata is included with Hyrax::ValkyrieWorkIndexer + include Hyrax::Indexer(:base_metadata) + include Spot::IndexesPermalinkUrl + include Spot::IndexesRightsStatementsAndLabels + include Spot::RemoteLabelIndexing + + class_attribute :sortable_date_property, default: :date + + def to_solr + super.tap do |document| + document['title_sort_si'] = generate_sortable_title + document['date_sort_dtsi'] = generate_sortable_date + document['identifier_standard_ssim'] = wrapped_identifiers.select(&:standard?).map(&:to_s) + document['identifier_local_ssim'] = wrapped_identifiers.select(&:local?).map(&:to_s) + document['thumbnail_url_ss'] = index_thumbnail_url + document['file_format_ssim'] = file_set_mime_types + + fetch_and_attach_remote_labels_to(document, + field: :language, + controlled_vocabulary_class: Spot::Iso6391) + fetch_and_attach_remote_labels_to(document, + field: :location, + controlled_vocabulary_class: Spot::ControlledVocabularies::Location, + label_key: ['location_label_ssim', 'location_label_tesim']) + fetch_and_attach_remote_labels_to(document, + field: :subject, + controlled_vocabulary_class: Spot::ControlledVocabularies::AssignFastSubject, + label_key: ['subject_label_ssim', 'subject_label_tesim']) + + add_citation_metadata(document) if resource.try(:bibliographic_citation).present? + + # @note run this last + stringify_rdf_uris(document) + end + end + + private + + # Previously was a mixin (IndexesCitationMetadata); parses the first :bibliographic_citation + # value and adds the metadata to the Solr document. + # + # @param [SolrDocument,Hash] + # @return [void] + def add_citation_metadata(document) + raw = Array.wrap(resource.bibliographic_citation).first + citation = ::AnyStyle.parse(raw)&.first + return if citation.blank? || citation[:type].nil? + + document['citation_journal_title_ss'] = citation[:"container-title"]&.first + document['citation_volume_ss'] = citation[:volume]&.first + document['citation_issue_ss'] = citation[:issue]&.first + + # split pages on any type of hyphen (*waves fist at em and en dashes*) + first_page, last_page = citation[:pages]&.first&.split(/[-–—]/, 2) + document['citation_firstpage_ss'] = first_page + document['citation_lastpage_ss'] = last_page + end + + # Should come up empty for new works before files are attached, but otherwise returns + # the mime_types of all child file_sets. + # + # @return [Array] + def file_set_mime_types + file_sets = Hyrax.query_service.custom_queries.find_child_file_sets(resource: resource) + file_sets.map { |fs| Hyrax::FileSetTypeService.new(file_set: fs).mime_type } + end + + # Uses a) earliest date in +sortable_date_property+, b) resource's :created_at value to serve + # as the sort date for the object. Will return nil if neither of these are present. + # + # @return [String, nil] + def generate_sortable_date + object_date_value = resource.try(sortable_date_property).presence || resource.try(:created_at).try(:strftime, '%FT%TZ') + date_value = Array.wrap(object_date_value).sort.first + + Date.edtf(date_value).try(:strftime, '%FT%TZ') + end + + # @return [String] + def generate_sortable_title + resource.title.first.to_s.downcase.gsub(/^(an?|the)\s+/, '').strip + end + + def index_thumbnail_url + return if ENV['URL_HOST'].blank? + + host = ENV['URL_HOST'] + host = "https://#{host}" unless host.start_with?('http') + path = Hyrax::ThumbnailPathService.call(resource) + URI.join(host, path).to_s + end + + def wrapped_identifiers + @wrapped_identifiers ||= (resource.try(:identifier) || []).map { |id| Spot::Identifier.from_string(id) } + end + + # @todo maybe this is fixed down the road in Hyrax, but Hyrax::Indexer(:base_metadata) + # will index fields with `document[field] = resource.send(field)` which will lead + # to (at the very least) RDF::URI values on the way to Solr. I'm not sure if there + # are mechanisms in place in Hyrax/RSolr to stringify URI values before they get + # sent to Solr, but just in case they're not we'll at least stringify RDF::URIs. + def stringify_rdf_uris(document) + document.transform_values! do |values| + stringified_values = stringify_values(values) + values.is_a?(Array) ? stringified_values : stringified_values.first + end + end + + def stringify_values(values) + Array.wrap(values).map do |value| + case value + when RDF::Literal, RDF::URI + value.to_s + when ActiveTriples::Resource + value.rdf_subject + else + value + end + end + end +end diff --git a/app/indexers/concerns/indexes_language_and_label.rb b/app/indexers/concerns/indexes_language_and_label.rb index db5075ad6..efcacafa2 100644 --- a/app/indexers/concerns/indexes_language_and_label.rb +++ b/app/indexers/concerns/indexes_language_and_label.rb @@ -14,7 +14,7 @@ def generate_solr_document object.language.each do |lang| doc[value_field] << lang - doc[label_field] << Spot::ISO6391.label_for(lang) + doc[label_field] << Spot::Iso6391.label_for(lang) end end end diff --git a/app/indexers/concerns/indexes_seasonal_dates.rb b/app/indexers/concerns/indexes_seasonal_dates.rb new file mode 100644 index 000000000..d64dccad2 --- /dev/null +++ b/app/indexers/concerns/indexes_seasonal_dates.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true +module IndexesSeasonalDates + extend ActiveSupport::Concern + + included do + class_attribute :seasonal_date_field, default: 'english_language_date_teim' + class_attribute :date_property_for_seasonal_label, default: :date_issued + end + + def to_solr + super.tap do |document| + add_english_language_dates(document) + end + end + + def add_english_language_dates(doc) + doc[seasonal_date_field] = dates.map do |date| + begin + parsed = Date.parse(date) + rescue ArgumentError + next unless date.match?(/^\d{4}-\d{2}/) + parsed = Date.new(*date.split('-').map(&:to_i)) + end + + season_names_for_date(parsed) + spelled_out_for_date(parsed) + end.flatten.reject(&:blank?) + 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 + # + # @param date [#strftime, #year] + # @return [Array] + 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 + # spelled_out_dates_for_date(Date.parse('2019-02-08')) + # => ['February 8 2019', 'Feb 8 2019'] + # + # @param date [#strftime] + # @return [Array] + def spelled_out_for_date(date) + %w[%B %b].map { |month| date.strftime("#{month} %Y") } + end + + # Saves us the hassle of having to type out that long attribute + # + # @return [Array] + def dates + work_object.send(date_property_for_seasonal_label) + end + + # Doing this because we're sharing this code with old Hyrax indexers + # that refer to the resource as "object", which doesn't exist in ResourceIndexers + def work_object + try(:resource) || object + end +end diff --git a/app/indexers/concerns/spot/indexes_permalink_url.rb b/app/indexers/concerns/spot/indexes_permalink_url.rb new file mode 100644 index 000000000..a79fc52ef --- /dev/null +++ b/app/indexers/concerns/spot/indexes_permalink_url.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true +module Spot + # Mixin to index a permalink for a resource. Uses a Handle value (`hdl` prefix) stored in #identifier + # to generate the handle.net URL, otherwise uses the work's URL within the application. + module IndexesPermalinkUrl + def to_solr + super.tap do |document| + break document if permalink.nil? + document['permalink_ss'] = permalink + end + end + + def handle_identifier + @handle_identifier ||= begin + id = resource.identifier.find { |value| value.start_with? 'hdl:' } + return unless id + Spot::Identifier.from_string(id) + end + end + + def permalink + @permalink ||= + handle_identifier ? "http://hdl.handle.net/#{handle_identifier.value}" : resource_url + end + + def resource_url + Rails.application.routes.url_helpers.polymorphic_url(resource, host: ENV['URL_HOST']) + end + end +end diff --git a/app/indexers/concerns/spot/indexes_rights_statements_and_labels.rb b/app/indexers/concerns/spot/indexes_rights_statements_and_labels.rb new file mode 100644 index 000000000..302cd66ec --- /dev/null +++ b/app/indexers/concerns/spot/indexes_rights_statements_and_labels.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +module Spot + module IndexesRightsStatementsAndLabels + def to_solr + super.tap do |document| + document['rights_statement_ssim'] = [] + document['rights_statement_label_ssim'] = [] + document['rights_statement_shortcode_ssim'] = [] + + Array.wrap(resource.rights_statement).each do |original_uri| + value = original_uri.to_s + label = rights_service.label(value) { value } + shortcode = rights_service.shortcode(value) { nil } + + document['rights_statement_ssim'] << value + document['rights_statement_label_ssim'] << label + document['rights_statement_shortcode_ssim'] << shortcode + end + end + end + + def rights_service + @rights_service ||= Hyrax.config.rights_statement_service_class.new + end + end +end diff --git a/app/indexers/concerns/spot/remote_label_indexing.rb b/app/indexers/concerns/spot/remote_label_indexing.rb new file mode 100644 index 000000000..46c3ffe17 --- /dev/null +++ b/app/indexers/concerns/spot/remote_label_indexing.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +module Spot + # Provides a helper method to the calling indexer for fetching labels from a remote source. + module RemoteLabelIndexing + extend ActiveSupport::Concern + + # @param [SolrDocument,Hash] document + # Document to append + # @option [Symbol] field + # Field to call on the resource + # @option [Class] controlled_vocabulary_class + # Class used to wrap the URI value and fetch a remote label. this is typically + # a descendant of ActiveTriples::Resource, but is expected to be initialized with + # a URI or String value and respond to :preferred_label. + # @option [String, Array] value_key + # Key (or array of keys) to store the field's original value + # @option [String, Array] label_key + # Key (or array of keys) to store the fetched value (fallback is original value) + # + # @return void + def fetch_and_attach_remote_labels_to(document, field:, controlled_vocabulary_class: nil, value_key: nil, label_key: nil) + value_key = Array.wrap(value_key || "#{field}_ssim") + label_key = Array.wrap(label_key || "#{field}_label_ssim") + + values = resource.try(field) || [] + return if values.empty? + + value_label_pairs = value_label_pairs_from(values, wrapper_class: controlled_vocabulary_class) + + original_values = value_label_pairs.map(&:first) + label_values = value_label_pairs.map(&:last) + + value_key.each { |vk| document[vk] = original_values.dup } + label_key.each { |lk| document[lk] = label_values.dup } + end + + private + + # Returns an array of tuples: uri and label + def value_label_pairs_from(values, wrapper_class: nil) + values.map do |value| + uri = case value + when RDF::URI + value.to_s + else + value # leave as-is + end + + wrapped_uri = wrapper_class ? wrapper_class.new(uri) : value + label = wrapped_uri.try(:preferred_label) || uri + + [uri, label] + end + end + end +end diff --git a/app/indexers/image_resource_indexer.rb b/app/indexers/image_resource_indexer.rb new file mode 100644 index 000000000..dd724ab7f --- /dev/null +++ b/app/indexers/image_resource_indexer.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +# +# Indexer for ImageResource objects. +# +# @todo add handling for years_encompassed_iim +class ImageResourceIndexer < BaseResourceIndexer + include Hyrax::Indexer(:image_metadata) + + self.sortable_date_property = :date +end diff --git a/app/indexers/publication_resource_indexer.rb b/app/indexers/publication_resource_indexer.rb new file mode 100644 index 000000000..99111bad1 --- /dev/null +++ b/app/indexers/publication_resource_indexer.rb @@ -0,0 +1,9 @@ +# frozen-string_literal: true +class PublicationResourceIndexer < BaseResourceIndexer + include Hyrax::Indexer(:institutional_metadata) + include Hyrax::Indexer(:publication_metadata) + + include IndexesSeasonalDates + + self.sortable_date_property = :date_issued +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..8a59d2077 --- /dev/null +++ b/app/indexers/student_work_resource_indexer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +class StudentWorkResourceIndexer < BaseResourceIndexer + include Hyrax::Indexer(:institutional_metadata) + include Hyrax::Indexer(:student_work_metadata) + + self.sortable_date_property = :date +end diff --git a/app/inputs/concerns/language_tagging.rb b/app/inputs/concerns/language_tagging.rb index 5ff89dcf5..46c42efef 100644 --- a/app/inputs/concerns/language_tagging.rb +++ b/app/inputs/concerns/language_tagging.rb @@ -50,7 +50,7 @@ def hint(_wrapper_options = nil) default_hint = super return unless default_hint - "#{default_hint} #{hint_text}".html_safe + "#{default_hint} #{hint_text}".html_safe end private @@ -63,15 +63,19 @@ def hint(_wrapper_options = nil) # @param [RDF::Literal, String] raw_value # @param [Integer] _index not used # @return [String] HTML content for field row + # + # @note the rows in bootstrap 4 aren't expanding to the full-width of the wrapping container, + # giving a squished appearance. how can we fix this? + # @see https://getbootstrap.com/docs/4.6/layout/grid/#no-gutters ? def build_field(raw_value, index = nil) value, language = parse_value(raw_value) <<-HTML -
-
+
+
#{build_input(value, index)}
-
+
#{build_language_autocomplete(language)}
@@ -145,7 +149,7 @@ def build_field_options(value, _index) # @todo move this to a locale # @return [String] def hint_text - 'This field may be tagged with a language' + 'This field may be tagged with a language.' end # Generates a name property for the autocomplete input. diff --git a/app/inputs/multi_authority_controlled_vocabulary_input.rb b/app/inputs/multi_authority_controlled_vocabulary_input.rb index a80e199a9..35b55bd95 100644 --- a/app/inputs/multi_authority_controlled_vocabulary_input.rb +++ b/app/inputs/multi_authority_controlled_vocabulary_input.rb @@ -7,14 +7,12 @@ # # app/views/records/edit_fields/_some_field.html.erb # # note: the values for the "authorities" property are the ids # # in config/authorities/remote_authorities.yml -# <%= -# f.input :some_field, -# as: :multi_authority_controlled_vocabulary, -# placeholder: 'Search for a value', -# authorities: [:geonames, :fast], -# wrapper_html: { data: { 'field-name' => 'some_field' } }, -# required: f.object.required?(:some_field) -# %> +# <%= f.input :some_field, +# as: :multi_authority_controlled_vocabulary, +# placeholder: 'Search for a value', +# authorities: [:geonames, :fast], +# wrapper_html: { data: { 'field-name' => 'some_field' } }, +# required: f.object.required?(:some_field) %> # class MultiAuthorityControlledVocabularyInput < ControlledVocabularyInput def input_type @@ -77,7 +75,8 @@ 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] + # col.reject { |value| value.respond_to?(:node?) ? value.node? : value.to_s.strip.blank? } + [cv_klass.new] + col.reject { |value| value.try(:node?) == true } end # class name of the controlled vocabulary for this property diff --git a/app/inputs/standard_identifier_input_group_input.rb b/app/inputs/standard_identifier_input_group_input.rb index 0a70244bb..fc20b9f97 100644 --- a/app/inputs/standard_identifier_input_group_input.rb +++ b/app/inputs/standard_identifier_input_group_input.rb @@ -29,7 +29,7 @@ def input_type # @param [Integer] index # @return [String] HTML output def build_field(raw_value, index) - identifier = Spot::Identifier.from_string(raw_value) + identifier = Spot::Identifier.from_string(raw_value.to_s) <<-HTML
diff --git a/app/jobs/mint_handle_job.rb b/app/jobs/mint_handle_job.rb index 2d388609d..82a18d20c 100644 --- a/app/jobs/mint_handle_job.rb +++ b/app/jobs/mint_handle_job.rb @@ -4,6 +4,7 @@ # to do the heavy-lifting. class MintHandleJob < ApplicationJob def perform(work) + work = Hyrax.query_service.find_by_alternate_identifier(alternate_identifier: work) if work.is_a?(String) Spot::HandleService.new(work).mint end end diff --git a/app/jobs/spot/regenerate_thumbnail_job.rb b/app/jobs/spot/regenerate_thumbnail_job.rb index 223cf2c69..4ac01726a 100644 --- a/app/jobs/spot/regenerate_thumbnail_job.rb +++ b/app/jobs/spot/regenerate_thumbnail_job.rb @@ -5,16 +5,19 @@ module Spot # basically a whole lot of work that we might not need to repeat. class RegenerateThumbnailJob < ApplicationJob def perform(work) + @work = work return if work&.thumbnail_id.nil? - file_set = FileSet.find(work.thumbnail_id) + file_set = Hyrax.query_service.find_by_alternate_identifier(alternate_identifier: work.thumbnail_id) filename = Hyrax::DerivativePath.derivative_path_for_reference(file_set, 'thumbnail') Spot::Derivatives::ThumbnailService.new(file_set).create_derivatives(filename) - file_set.reload - file_set.update_index - work.update_index + # I think we need to at least persist the Resource so that the updated thumbnail path is saved + [work, file_set].each { |obj| Hyrax.persister.save(resource: obj) } + true + rescue ::Valkyrie::Persistence::ObjectNotFoundError, ActiveFedora::ObjectNotFoundError, Ldp::Gone + Rails.logger.warn("Unable to regenerate thumbnail for deleted work #{@work.id}") true end end diff --git a/app/jobs/spot/repository_fixity_check_job.rb b/app/jobs/spot/repository_fixity_check_job.rb index 7d94d349f..4bbcede13 100644 --- a/app/jobs/spot/repository_fixity_check_job.rb +++ b/app/jobs/spot/repository_fixity_check_job.rb @@ -11,22 +11,31 @@ # # Running the checks this way allows us to send a follow-up email/post # when the jobs are done running. +# +# @todo Is this service still useful in a Valkyrized world? Are there other ways +# to be doing this now? module Spot class RepositoryFixityCheckJob < ApplicationJob queue_as :low_priority around_perform :wrap_check - # @param [true, false] :force Ignore the 'max days between check' parameter + # @param [Hash] options + # @option [true, false] :force Ignore the 'max days between check' parameter (default is false) + # @return [void] + # + # @todo because we're running this with the `async_jobs: false` option, we should + # have access to the ChecksumAuditLog objects that are generated in + # Hyrax::FileSetFixityCheckService. Do we want to do anything with that info? def perform(force: false) opts = { async_jobs: false } opts[:max_days_between_fixity_checks] = -1 if force @count = 0 - ::FileSet.find_each do |file_set| + Hyrax.query_service.find_all_of_model(model: Hyrax::FileSet).each do |file_set| @count += 1 - Hyrax::FileSetFixityCheckService.new(file_set, opts).fixity_check + Hyrax::FileSetFixityCheckService.new(file_set, **opts).fixity_check end end diff --git a/app/jobs/spot/sync_collection_permissions_job.rb b/app/jobs/spot/sync_collection_permissions_job.rb index 065fd12c5..d11075c0c 100644 --- a/app/jobs/spot/sync_collection_permissions_job.rb +++ b/app/jobs/spot/sync_collection_permissions_job.rb @@ -14,8 +14,11 @@ module Spot # collection = Collection.find('abc123def') # Spot::SyncCollectionPermissionsJob.perform_later(collection, reset: true) # + # + # @todo Rewrite to use Hyrax ACL objects instead? + # @see https://github.com/samvera/hyrax/wiki/Hyrax-Valkyrie-Usage-Guide#permissions class SyncCollectionPermissionsJob < ApplicationJob - # @param [Collection] + # @param [Collection, Hyrax::PcdmCollection] # @param [Hash] options # @option [true, false] reset def perform(collection, reset: false) @@ -25,7 +28,7 @@ def perform(collection, reset: false) reset_permissions_for(member) if reset == true Hyrax::PermissionTemplateApplicator.apply(template).to(model: member) - member.save + member.permission_manager.acl.save # @note this will save the member object as well end true @@ -35,9 +38,10 @@ def perform(collection, reset: false) # Convenience method to make our lives easier when switching to Valkyrie. # - # @param [Collection] collection + # @param [Collection, Hyrax::PcdmCollection] collection + # @return [Array] def members_of(collection) - ActiveFedora::Base.where(member_of_collection_ids_ssim: collection.id) + Hyrax.query_service.custom_queries.find_members_of(collection: collection) end # Clears out edit groups/users and read groups/users for an item. Since permission_templates diff --git a/app/models/ability.rb b/app/models/ability.rb index f28f9d9b2..063f8ae21 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -27,10 +27,18 @@ def self.preload_roles! # @return [void] def custom_permissions; end + # Added by Bulkrax generator + # + # @return [true, false] + # @see https://github.com/samvera/bulkrax/blob/v9.1.0/lib/generators/bulkrax/install_generator.rb#L58-L76 def can_import_works? current_user.admin? || current_user.depositor? end + # Added by Bulkrax generator + # + # @return [true, false] + # @see https://github.com/samvera/bulkrax/blob/v9.1.0/lib/generators/bulkrax/install_generator.rb#L58-L76 def can_export_works? current_user.admin? || current_user.depositor? end @@ -47,13 +55,14 @@ def admin_abilities can(role_abilities, Role) # admins can create everything - can(:create, curation_concerns_models) + can([:create, :delete, :manage], curation_concerns_models) + can([:create, :delete, :edit, :discover, :read, :manage], [PublicationResource, ImageResource, StudentWorkResource, AudioVisualResource]) end def authenticated_users_can_deposit_student_works return unless registered_user? - can(:create, StudentWork) + can(:create, [StudentWork, StudentWorkResource]) end # Delegates abilities for users that have the 'depositor' role @@ -63,7 +72,7 @@ def authenticated_users_can_deposit_student_works def depositor_abilities return unless current_user.depositor? - can(:create, Publication) + can(:create, [Publication, PublicationResource]) # can view the user dashboard can(:read, :dashboard) @@ -87,7 +96,7 @@ def faculty_abilities def student_abilities return unless current_user.student? - can(:create, StudentWork) + can(:create, [StudentWork, StudentWorkResource]) can(:read, :dashboard) end diff --git a/app/models/audio_visual_resource.rb b/app/models/audio_visual_resource.rb new file mode 100644 index 000000000..3cf3507f3 --- /dev/null +++ b/app/models/audio_visual_resource.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +class AudioVisualResource < ::Hyrax::Work + include Hyrax::Schema(:base_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + include Hyrax::Schema(:audio_visual_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + + attribute :stored_derivatives, Valkyrie::Types::String +end diff --git a/app/models/concerns/spot/base_resource_behavior.rb b/app/models/concerns/spot/base_resource_behavior.rb new file mode 100644 index 000000000..ad7ef36fe --- /dev/null +++ b/app/models/concerns/spot/base_resource_behavior.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +module Spot + module BaseResourceBehavior + # Wraps stored :identifier values (strings) in our {Spot::Identifier} interface + # + # @see app/models/spot/identifier.rb + # @return [Array] + def identifier + Array.wrap(self[:identifier]).map { |id| Spot::Identifier.from_string(id) } + end + + # Wraps stored :location values (themselves wrapped in RDF::URI via Hyrax/Valkyrie) in + # our {Spot::ControlledVocabularies::Location} interface. This conforms to the previous + # ActiveFedora interface and gives us an entrypoint to fetch labels during indexing. + # + # @see app/models/spot/controlled_vocabularies/location.rb + # @return [Array] + def location + Array.wrap(self[:location]).map { |uri| Spot::ControlledVocabularies::Location.new(uri) } + end + + # Wraps stored :subject values (themselves wrapped in RDF::URI via Valkyrie) in + # our {Spot::ControlledVocabularies::AssignFastSubject} interface. This conforms to the previous + # ActiveFedora interface and gives us an entrypoint to fetch labels during indexing. + # + # @see app/models/spot/controlled_vocabularies/assign_fast_subject.rb + # @return [Array] + def subject + Array.wrap(self[:subject]).map { |uri| Spot::ControlledVocabularies::AssignFastSubject.new(uri) } + end + end +end diff --git a/app/models/concerns/spot/nested_collection_behavior.rb b/app/models/concerns/spot/nested_collection_behavior.rb index 631ddb6ba..dbfda9f02 100644 --- a/app/models/concerns/spot/nested_collection_behavior.rb +++ b/app/models/concerns/spot/nested_collection_behavior.rb @@ -33,7 +33,7 @@ def add_member_objects(new_member_ids) collection_ids_to_add = collections_to_add.map(&:id) Array(new_member_ids).collect do |member_id| - member = member_query_service(member_id) + member = find_member_by_id(member_id) message = check_multiple_membership(item: member, collection_ids: collection_ids_to_add) if message @@ -58,7 +58,6 @@ def gather_collections_to_add [].tap do |collections| until collections_to_check.size.zero? col = collections_to_check.shift - col.reindex_extent = Hyrax::Adapters::NestingIndexAdapter::LIMITED_REINDEX collections << col unless collections.include?(col) collections_to_check += col.member_of_collections @@ -66,15 +65,9 @@ def gather_collections_to_add end end - # Hyrax@3 uses +Hyrax.query_service.find_by_alternate_id+ to fetch an object. Hyrax@2 - # uses +ActiveFedora::Base.find+. This ought to allow us to upgrade without a fuss - # (at least as far as this code is concerned). We can replace this after the upgrade. - # # @param [String] id - # @return [ActiveFedora::Base] - # @todo replace with just a call to +find_by_alternate_id+ after we upgrade to hyrax@3 - # and start switching to Wings - def member_query_service(id) + # @return [Hyrax::Resource] + def find_member_by_id(id) Hyrax.query_service.find_by_alternate_identifier(alternate_identifier: id, use_valkyrie: false) end diff --git a/app/models/concerns/spot/solr_document_attributes.rb b/app/models/concerns/spot/solr_document_attributes.rb index 84d37f80f..9739532a2 100644 --- a/app/models/concerns/spot/solr_document_attributes.rb +++ b/app/models/concerns/spot/solr_document_attributes.rb @@ -3,10 +3,6 @@ module Spot # Various model attributes for the single SolrDocument model. Since that model wraps # every kind of Solr document, we need to ensure that _all_ of the models attributes # are available. - # - # - # - # @todo Remove {.attribute} definition when Hyrax upgrades to Blacklight >=7 module SolrDocumentAttributes extend ActiveSupport::Concern @@ -106,14 +102,17 @@ module SolrDocumentAttributes attribute :citation_issue, ::Blacklight::Types::String, 'citation_issue_ss' attribute :citation_firstpage, ::Blacklight::Types::String, 'citation_firstpage_ss' attribute :citation_lastpage, ::Blacklight::Types::String, 'citation_lastpage_ss' - end - module ClassMethods - def attribute(name, type, field) - define_method name do - type.coerce(self[field]) - end - end + # oai attributes + attribute :file_format, ::Blacklight::Types::Array, 'file_format_ssim' end + + # module ClassMethods + # def attribute(name, type, field) + # define_method name do + # type.coerce(self[field]) + # end + # end + # end end end diff --git a/app/models/concerns/spot/work_behavior.rb b/app/models/concerns/spot/work_behavior.rb index 1490f78cc..262a14761 100644 --- a/app/models/concerns/spot/work_behavior.rb +++ b/app/models/concerns/spot/work_behavior.rb @@ -32,6 +32,12 @@ module WorkBehavior field: :rights_statement, authority: 'rights_statements' end + # WorkType#label is called within Hyrax::Serializers#to_s if a work is missing a title. + # This just enusres that the check doesn't thorw a NoMethodError. + # + # @see https://github.com/samvera/hyrax/blob/hyrax-v4.0.0/app/models/concerns/hyrax/serializers.rb#L7 + def label; end + module ClassMethods # Intended to be called at the end of your model to setup +accepts_nested_attributes_for+ # for your controlled properties. Uses the +controlled_properties+ attribute. diff --git a/app/models/image_resource.rb b/app/models/image_resource.rb new file mode 100644 index 000000000..9de64fee0 --- /dev/null +++ b/app/models/image_resource.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true +class ImageResource < ::Hyrax::Work + include Hyrax::Schema(:base_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + include Hyrax::Schema(:image_metadata, schema_loader: Spot::SimpleSchemaLoader.new) +end diff --git a/app/models/publication_resource.rb b/app/models/publication_resource.rb new file mode 100644 index 000000000..2111b0dd3 --- /dev/null +++ b/app/models/publication_resource.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +class PublicationResource < ::Hyrax::Work + include Hyrax::Schema(:base_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + include Hyrax::Schema(:institutional_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + include Hyrax::Schema(:publication_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + + include Spot::BaseResourceBehavior +end diff --git a/app/models/solr_document.rb b/app/models/solr_document.rb index 9d5ad20c8..7ae8a2e7b 100644 --- a/app/models/solr_document.rb +++ b/app/models/solr_document.rb @@ -1,6 +1,4 @@ # frozen_string_literal: true -# -# Generated from +rails generate hyrax:install+ class SolrDocument include Blacklight::Solr::Document include BlacklightOaiProvider::SolrDocument @@ -39,13 +37,14 @@ class SolrDocument # mapped to dc:identifier. Will this just work out of the box? # @todo PA Digital guidelines specify ISO 639-3 for language (we're using 639-1); # will we need to revisit that? + def self.field_semantics { collection_name: 'member_of_collections_ssim', contributor: 'contributor_tesim', - coverage: 'location_label_ssim', + coverage: 'location_label_tesim', creator: 'creator_tesim', - date: 'date_issued_ssim', + date: ['date_issued_ssim', 'date_ssim'], description: 'description_tesim', format: 'file_format_ssim', identifier: ['id', 'permalink_ss', 'thumbnail_url_ss'], diff --git a/app/models/spot/oai_collection_solr_set.rb b/app/models/spot/oai_collection_solr_set.rb index bf04ed476..d9c24ae7a 100644 --- a/app/models/spot/oai_collection_solr_set.rb +++ b/app/models/spot/oai_collection_solr_set.rb @@ -2,27 +2,75 @@ require 'cgi' module Spot - # Subclass of +BlacklightOaiProvider::SolrSet+ that translates - # spaces to underscores, making the following assumptions: - # a) we'll be using +member_of_collections_ssim+ as our only hook for OAI-PMG ListSets - # b) collection titles will never contain underscores class OaiCollectionSolrSet < ::BlacklightOaiProvider::SolrSet - def self.sets_for(record) - @fields = CatalogController.blacklight_config.oai[:document][:set_fields] if @fields.nil? + class << self + # Rewriting this method bc the faceting approach used in BlacklightOaiProvider::SolrSet + # stopped working somewhere along the upgrade path. From the best I can surmise, the existing + # search builder code used (via Spot::CatalogSearchBuilder + a long inheritance chain) will + # limit the collection ids in a facet query to those available to the user + # + # @note if we wanted to limit which collections are being offered to the OAI service, this + # this is the location where that would happen. + # @return [Array user_key) + end + # Does this user belong to the Alumni group? # # @return [true, false] diff --git a/app/presenters/concerns/spot/file_set_presenter_additions.rb b/app/presenters/concerns/spot/file_set_presenter_additions.rb new file mode 100644 index 000000000..3b59e5d2d --- /dev/null +++ b/app/presenters/concerns/spot/file_set_presenter_additions.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +module Spot + module FileSetPresenterAdditions + extend ActiveSupport::Concern + + included do + delegate :original_filenames, :transcript_name, :stored_derivatives, to: :solr_document + end + end +end 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..9ee1be669 --- /dev/null +++ b/app/presenters/concerns/spot/lucene_patch_for_pcdm_member_presenters_factory.rb @@ -0,0 +1,24 @@ +# 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-v4.0.0/app/presenters/hyrax/pcdm_member_presenter_factory.rb#L109-L117 + module LucenePatchForPcdmMemberPresentersFactory + private + + def query_docs(generic_type: nil, ids: object.member_ids) + query = "{!terms f=id}#{ids.join(',')}" + query += "{!term f=generic_type_si}#{generic_type}" if generic_type + # works created via ActiveFedora use the _sim field + query += "{!term f=generic_type_sim}#{generic_type}" if generic_type + + Hyrax::SolrService + .post(q: query, rows: 10_000, defType: 'lucene') + .fetch('response') + .fetch('docs') + end + end +end diff --git a/app/presenters/concerns/spot/presents_attributes.rb b/app/presenters/concerns/spot/presents_attributes.rb index 5f21ebc85..210ccfe97 100644 --- a/app/presenters/concerns/spot/presents_attributes.rb +++ b/app/presenters/concerns/spot/presents_attributes.rb @@ -5,8 +5,6 @@ # without having to namespace to +Hyrax::Renderers+, though we'll also fall back # to a super +#renderer_for+ if it exists. # -# Note that this does not replace any of the other methods defined by the Hyrax -# mixin. It is simply a less-monkey-patchy way of changing the renderer selector. module Spot module PresentsAttributes # Identical to +Hyrax::PresentsAttributes#attribute_to_html+, but removes diff --git a/app/presenters/spot/base_presenter.rb b/app/presenters/spot/base_presenter.rb index 0a0190b01..d8a203128 100644 --- a/app/presenters/spot/base_presenter.rb +++ b/app/presenters/spot/base_presenter.rb @@ -35,8 +35,9 @@ class BasePresenter < ::Hyrax::WorkShowPresenter # :nocov: def download_url return '' if representative_presenter.blank? - Hyrax::Engine.routes.url_helpers.download_url(representative_presenter, host: request.host, protocol: 'https://') + Hyrax::Engine.routes.url_helpers.download_url(representative_id, host: request.host, protocol: 'https://') end + # :nocov: # @return [String] def export_all_text @@ -86,6 +87,19 @@ def page_title "#{title.first} // #{I18n.t('hyrax.product_name')}" end + # Replacing Hyrax::PermissionBadge with our own subclass that can handle :metadata visibility + # + # @return [Spot::PermissionBadge] + def permission_badge_class + Spot::PermissionBadge + end + + def representative_presenter + super + rescue Hyrax::ObjectNotFoundError, ArgumentError + nil + end + # @return [Array>] def rights_statement_merged solr_document.rights_statement.zip(solr_document.rights_statement_label) diff --git a/app/presenters/spot/permission_badge.rb b/app/presenters/spot/permission_badge.rb new file mode 100644 index 000000000..3f0da24d5 --- /dev/null +++ b/app/presenters/spot/permission_badge.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +module Spot + class PermissionBadge < Hyrax::PermissionBadge + VISIBILITY_LABEL_CLASS = { + authenticated: "badge-info", + embargo: "badge-warning", + lease: "badge-warning", + metadata: "badge-info", + open: "badge-success", + restricted: "badge-danger" + }.freeze + end +end diff --git a/app/renderers/spot/renderers/attribute_renderer.rb b/app/renderers/spot/renderers/attribute_renderer.rb index 2adb3d8a2..ab73c01bd 100644 --- a/app/renderers/spot/renderers/attribute_renderer.rb +++ b/app/renderers/spot/renderers/attribute_renderer.rb @@ -121,7 +121,7 @@ def label_with_help_text(label_text) # @return [String, nil] def help_text - translate(:"simple_form.hints.defaults.#{field.downcase}", default: nil) + I18n.translate(:"simple_form.hints.defaults.#{field.downcase}", default: '') end # We need to stuff a value in case +options[:work_type]+ isn't provided, diff --git a/app/search_builders/search_builder.rb b/app/search_builders/search_builder.rb index 659276528..bed8b2125 100644 --- a/app/search_builders/search_builder.rb +++ b/app/search_builders/search_builder.rb @@ -11,6 +11,17 @@ class SearchBuilder < Blacklight::SearchBuilder include Hydra::AccessControlsEnforcement include Hyrax::SearchFilters + # Overriding Hyrax::SearchBuilder#work_types to include our Valkyrized Resource classes, + # rather than registering them in config/initializers/hyrax.rb because registering adds + # unwanted behaviors (such as adding dupicate work types to the create works modal). + # + # @return [Array] + def work_types + Hyrax.config.curation_concerns.flat_map do |concern_class| + [concern_class, "#{concern_class}Resource".safe_constantize].compact + end + end + ## # @example Adding a new step to the processor chain # self.default_processor_chain += [:add_custom_data_to_query] diff --git a/app/search_builders/spot/catalog_search_builder.rb b/app/search_builders/spot/catalog_search_builder.rb index b6f5b53d5..400134116 100644 --- a/app/search_builders/spot/catalog_search_builder.rb +++ b/app/search_builders/spot/catalog_search_builder.rb @@ -5,7 +5,7 @@ module Spot # the Hyrax join query for extracted text, and conditionally (for now) # display highlight matches (where present). class CatalogSearchBuilder < ::Hyrax::CatalogSearchBuilder - include BlacklightAdvancedSearch::AdvancedSearchBuilder + # include BlacklightAdvancedSearch::AdvancedSearchBuilder include BlacklightRangeLimit::RangeLimitBuilder self.default_processor_chain -= [:show_works_or_works_that_contain_files] diff --git a/app/services/concerns/spot/derivatives/iiif_access_copy_behavior.rb b/app/services/concerns/spot/derivatives/iiif_access_copy_behavior.rb new file mode 100644 index 000000000..b1c725a0e --- /dev/null +++ b/app/services/concerns/spot/derivatives/iiif_access_copy_behavior.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true +require 'fileutils' + +# 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 +module Spot + module Derivatives + module IiifAccessCopyBehavior + # 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) + if ENV['AWS_IIIF_ASSET_BUCKET'].blank? + Rails.logger.warn('Skipping IIIF Access Copy generation because the AWS_IIIF_ASSET_BUCKET environment variable is not defined.') + return false + end + + create_access_copy_from(filename) + upload_derivatives_to_s3 && FileUtils.rm_f(shuttle_file) if File.exist?(shuttle_file) + end + + # 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 delete_iiif_access_copy! + storage_adapter.delete(id: File.basename(shuttle_file)) + end + + private + + 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 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/rdf_literal_serializer.rb b/app/services/rdf_literal_serializer.rb index 093d2640e..e1fe84140 100644 --- a/app/services/rdf_literal_serializer.rb +++ b/app/services/rdf_literal_serializer.rb @@ -1,31 +1,43 @@ # frozen_string_literal: true require 'rdf/ntriples' +# Helper class to serialize RDF literal values. +# +# @example +# RdfLiteralSerializer.serialize(RDF::Literal.new('Cool Beans', language: :eng)) +# # => "\"Cool Beans\"@eng" +# +# @example +# RdfLiteralSerializer.deserialize('"Cool Beans"@eng') +# # => # +# +# @todo Move this into Spot namespace? +# @todo Support for types other than :ntriples? :ttl fails to parse, for example +# (NoMethodError for 'parse_literal' in the Turtle reader) class RdfLiteralSerializer + include Singleton + + class_attribute :type, default: :ntriples + + # @param [RDF::Literal] literal # @return [String] - def serialize(literal) - writer.format_literal(literal) + def self.serialize(literal) + instance.writer.format_literal(literal) end + # @param [String] input_string # @return [RDF::Literal] - def deserialize(string) - reader.parse_literal(string) - end - - private - - # @return [Symbol] - def type - :ntriples + def self.deserialize(input_string) + instance.reader.parse_literal(input_string) end # @return [RDF::Reader] def reader - @reader ||= RDF::Reader.for(type) + RDF::Reader.for(type) end # @return [RDF::Writer] def writer - @writer ||= RDF::Writer.for(type).new + RDF::Writer.for(type).new end end diff --git a/app/services/spot/characterization_service.rb b/app/services/spot/characterization_service.rb index 53de286d6..eea012fe6 100644 --- a/app/services/spot/characterization_service.rb +++ b/app/services/spot/characterization_service.rb @@ -10,9 +10,7 @@ module Spot class CharacterizationService < ::Hydra::Works::CharacterizationService def self.run(characterization_proxy, filepath, opts = {}) tool = ENV['FITS_SERVLET_URL'].present? ? :fits_servlet : :fits - opts = { ch12n_tool: tool }.merge(opts) - - super(characterization_proxy, filepath, opts) + super(characterization_proxy, filepath, { ch12n_tool: tool }.merge(opts)) end end end diff --git a/app/services/spot/deep_indexing_service.rb b/app/services/spot/deep_indexing_service.rb index 6b01694b6..3175aac02 100644 --- a/app/services/spot/deep_indexing_service.rb +++ b/app/services/spot/deep_indexing_service.rb @@ -19,6 +19,8 @@ # path, rather than just using +Hyrax::DeepIndexingService+ # because of a requirement of +Hyrax::BasicMetadata+ fields, # some of which we're excluding. +# +# @todo (2024-11-10) Is this necessary for Valkyrization? How are we handling RDF indexing? module Spot class DeepIndexingService < ActiveFedora::RDF::IndexingService # Called from within {ActiveFedora::RDF::IndexingService#add_assertions} diff --git a/app/services/spot/derivatives/audio_derivative_service.rb b/app/services/spot/derivatives/audio_derivative_service.rb index 6901c6c31..bd8256724 100644 --- a/app/services/spot/derivatives/audio_derivative_service.rb +++ b/app/services/spot/derivatives/audio_derivative_service.rb @@ -61,7 +61,7 @@ def rename_premade_derivative(derivative, index) s3_client.get_object(key: derivative, bucket: s3_source, response_target: file_path) # add any other checks to the file here - key = format('%s-%d-access.mp3', file_set.id, index) + key = format('%s-%d-access.mp3', file_set_resource.id, index) FileUtils.rm_f(file_path) if File.exist?(file_path) transfer_s3_derivative(derivative, key) end @@ -91,7 +91,7 @@ def create_derivative_files(filename) # Keys for generated derivatives. def s3_derivative_keys - [format('%s-0-access.mp3', file_set.id)] + [format('%s-0-access.mp3', file_set_resource.id)] end end end diff --git a/app/services/spot/derivatives/audio_visual_base_derivative_service.rb b/app/services/spot/derivatives/audio_visual_base_derivative_service.rb index aafa15c66..8c4a0aeea 100644 --- a/app/services/spot/derivatives/audio_visual_base_derivative_service.rb +++ b/app/services/spot/derivatives/audio_visual_base_derivative_service.rb @@ -27,7 +27,7 @@ class AudioVisualBaseDerivativeService < BaseDerivativeService # @return [void] # @see https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#delete_object-instance_method def cleanup_derivatives - prefix = file_set.id + "-" + prefix = file_set_resource.id + "-" object_list = s3_client.list_objects(bucket: s3_bucket, prefix: prefix).to_h[:contents] return if object_list.nil? @@ -55,6 +55,11 @@ def valid? audio_mime_types.include?(mime_type) || video_mime_types.include?(mime_type) end + def file_set_resource + @file_set_resource ||= + Hyrax.query_service.find_by_alternate_identifier(alternate_identifier: file_set.id) + end + private def no_bucket_warning @@ -94,7 +99,7 @@ def s3_derivative_keys; end # Uploads generated derivatives specified by paths with new names specified by # keys to the s3 bucket. Adds all uploaded keys to the stored_derivatives metadata field def upload_derivatives_to_s3(keys, paths) - stored_derivatives = file_set.stored_derivatives.to_a + stored_derivatives = file_set_resource.stored_derivatives.to_a paths.each_with_index do |path, index| stored_derivatives.push(keys[index]) s3_client.put_object( @@ -106,17 +111,17 @@ def upload_derivatives_to_s3(keys, paths) metadata: {} ) end - file_set.stored_derivatives = stored_derivatives - file_set.save + file_set_resource.stored_derivatives = stored_derivatives + Hyrax.persister.save(resource: file_set_resource) end # Transfers a single derviative from the source bucket to the destination bucket, renaming # it. Adds all uploaded keys to the stored_derivatives metadata field def transfer_s3_derivative(derivative, key) - stored_derivatives = file_set.stored_derivatives.to_a + stored_derivatives = file_set_resource.stored_derivatives.to_a stored_derivatives.push(key) - file_set.stored_derivatives = stored_derivatives - file_set.save + file_set_resource.stored_derivatives = stored_derivatives + Hyrax.persister.save(resource: file_set_resource) src = "/" + s3_source + "/" + derivative s3_client.copy_object( bucket: s3_bucket, diff --git a/app/services/spot/derivatives/base_derivative_service.rb b/app/services/spot/derivatives/base_derivative_service.rb index 0494520a9..0068cd829 100644 --- a/app/services/spot/derivatives/base_derivative_service.rb +++ b/app/services/spot/derivatives/base_derivative_service.rb @@ -1,14 +1,125 @@ # 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 + attr_reader :file_set + delegate :mime_type, to: :file_set + delegate :audio_mime_types, :image_mime_types, :pdf_mime_types, :office_document_mime_types, :video_mime_types, to: :FileSet + + def initialize(file_set) + @file_set = file_set + end + + def cleanup_derivatives + FileUtils.rm_f(thumbnail_derivative_path) if File.exist?(thumbnail_derivative_path) + end + + def create_derivatives(file_path) + create_thumbnail(file_path) + extract_and_save_full_text(file_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 full_text_eligible_types + [*image_mime_types, *pdf_mime_types, *office_document_mime_types] + end + + # Helper method to use MiniMagick to generate a 200x150 thumbnail + def create_thumbnail(src_path) + MiniMagick::Tool::Convert.new do |convert| + convert.merge!( + [ + "#{src_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? + + 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) + 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 + 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/iiif_access_copy_service.rb b/app/services/spot/derivatives/iiif_access_copy_service.rb index 704b01329..62f6165a5 100644 --- a/app/services/spot/derivatives/iiif_access_copy_service.rb +++ b/app/services/spot/derivatives/iiif_access_copy_service.rb @@ -16,49 +16,24 @@ module Derivatives # # @see https://www.loc.gov/preservation/digital/formats/fdd/fdd000237.shtml class IiifAccessCopyService < BaseDerivativeService - class_attribute :derivative_key_template - self.derivative_key_template = '%s-access.tif' - - # Deletes the derivative from the S3 bucket + # 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] - # @see https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#delete_object-instance_method def cleanup_derivatives - s3_client.delete_object(bucket: s3_bucket, key: s3_derivative_key) + 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. + # 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) - output_dirname = File.dirname(derivative_path) - FileUtils.mkdir_p(output_dirname) unless File.directory?(output_dirname) - - MiniMagick::Tool::Convert.new do |convert| - convert.merge!( - [ - "#{filename}[0]", - "-define", "tiff:tile-geometry=128x128", - "-compress", "jpeg", - "ptif:#{derivative_path}" - ] - ) - end - - upload_derivative_to_s3 + create_access_copy_from(filename) + upload_derivatives_to_s3 - FileUtils.rm_f(derivative_path) if File.exist?(derivative_path) - end - - # copied from https://github.com/samvera/hyrax/blob/5a9d1be1/app/services/hyrax/file_set_derivatives_service.rb#L32-L37 - # but modifies the filename it writes out to. - # - # @return [String] - def derivative_path - @derivative_path ||= - Hyrax::DerivativePath.derivative_path_for_reference(file_set, 'access.tif').to_s.gsub(/\.access\.tif$/, '') + FileUtils.rm_f(shuttle_file) if File.exist?(shuttle_file) end # Only create pyramidal TIFFs if the source mime_type is an Image and if we defined @@ -73,33 +48,48 @@ def valid? private + 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 s3_bucket ENV['AWS_IIIF_ASSET_BUCKET'] end - # We're using AWS credentials stored within the App/Sidekiq services for authentication, - # so the Aws::S3::Client will pick them up ambiently. - def s3_client - @s3_client ||= Aws::S3::Client.new + def shuttle_file + working_directory.join("#{file_set.id}-access.tif") end - def s3_derivative_key - derivative_key_template % file_set.id + def storage_adapter + Valkyrie::StorageAdapter.find(:iiif_source_s3) end - def upload_derivative_to_s3 - s3_client.put_object( - bucket: s3_bucket, - key: s3_derivative_key, - body: File.open(derivative_path, 'r'), - content_length: File.size(derivative_path), - content_md5: Digest::MD5.file(derivative_path).base64digest, + 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/image_derivative_service.rb b/app/services/spot/derivatives/image_derivative_service.rb new file mode 100644 index 000000000..efbf9cabd --- /dev/null +++ b/app/services/spot/derivatives/image_derivative_service.rb @@ -0,0 +1,104 @@ +# 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 && FileUtils.rm_f(shuttle_file) if File.exist?(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 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/video_derivative_service.rb b/app/services/spot/derivatives/video_derivative_service.rb index 7fb752836..3d7de9aa2 100644 --- a/app/services/spot/derivatives/video_derivative_service.rb +++ b/app/services/spot/derivatives/video_derivative_service.rb @@ -68,7 +68,7 @@ def rename_premade_derivative(derivative, index) s3_client.get_object(key: derivative, bucket: s3_source, response_target: file_path) res = get_video_resolution(file_path) # add any other checks to the file here - key = format('%s-%d-access-%d.mp4', file_set.id, index, res[1]) + key = format('%s-%d-access-%d.mp4', file_set_resource.id, index, res[1]) FileUtils.rm_f(file_path) if File.exist?(file_path) transfer_s3_derivative(derivative, key) end @@ -155,8 +155,8 @@ def create_derivative_files(filename) # Keys for generated derivatives. def s3_derivative_keys - [format('%s-0-access-1080.mp4', file_set.id), - format('%s-1-access-480.mp4', file_set.id)] + [format('%s-0-access-1080.mp4', file_set_resource.id), + format('%s-1-access-480.mp4', file_set_resource.id)] end end end diff --git a/app/services/spot/embargo_lease_service.rb b/app/services/spot/embargo_lease_service.rb index 061262742..232425588 100644 --- a/app/services/spot/embargo_lease_service.rb +++ b/app/services/spot/embargo_lease_service.rb @@ -3,15 +3,15 @@ module Spot # A service for dealing with embargoes and leases. Right now, we're just # using this to clear out expired items. # - # @example Clear out all expired values at once - # Spot::EmbargoLeaseService.clear_all_expired + # @example Clear out all expired values at once and regenerate thumbnails + # Spot::EmbargoLeaseService.clear_all_expired(regenerate_thumbnails: true) # - # @example Clear out expired embargoes (and update +date_available+ values) + # @example Clear out expired embargoes # Spot::EmbargoLeaseService.clear_expired_embargoes # # @example Clear out expired leases # Spot::EmbargoLeaseService.clear_expired_leases - # + class EmbargoLeaseService class << self # Convenience method to clear both embargoes and leases @@ -28,18 +28,7 @@ def clear_all_expired(regenerate_thumbnails: false) # @return [void] def clear_expired_embargoes(regenerate_thumbnails: false) ::Hyrax::EmbargoService.assets_with_expired_embargoes.each do |presenter| - item = ActiveFedora::Base.find(presenter.id) - - next if item.under_embargo? - - ::Hyrax::Actors::EmbargoActor.new(item).destroy - - next if item.is_a? FileSet - - item.copy_visibility_to_files - item.save! - - RegenerateThumbnailJob.perform_later(item) if regenerate_thumbnails == true + release_and_save_for_id(presenter.id, type: :embargo, regenerate_thumbnails: regenerate_thumbnails) end end @@ -48,15 +37,57 @@ def clear_expired_embargoes(regenerate_thumbnails: false) # @return [void] def clear_expired_leases(regenerate_thumbnails: false) ::Hyrax::LeaseService.assets_with_expired_leases.each do |presenter| - item = ActiveFedora::Base.find(presenter.id) + release_and_save_for_id(presenter.id, type: :lease, regenerate_thumbnails: regenerate_thumbnails) + end + end + + private - next if item.active_lease? + # Embargos and Leases behave very similarly and are managed through similar interfaces + # in Hyrax, so we'll do the bulk of that work here. + # + # @param [String] id + # @option [:embargo, :lease] :type + # @option [true/false] :regenerate_thumbnails + # @return [void] + def release_and_save_for_id(id, type:, regenerate_thumbnails: false) + resource = release_and_save_acl_for(type: type, resource: Hyrax.query_service.find_by_alternate_identifier(alternate_identifier: id)) + return if resource.nil? + + copy_visibility_to_members!(resource: resource) + + RegenerateThumbnailJob.perform_later(resource) if regenerate_thumbnails == true + end - ::Hyrax::Actors::LeaseActor.new(item).destroy + # calling #release! on the managers will raise a +NotReleaseableError+ if + # the embargo/lease isn't ready to be deactivated. Since the resource's + # visibility hasn't changed, we won't bother to resave the object or enqueue + # a thumbnail regeneration job. + # + # @todo how can we test the `nil` return value for coverage? + def release_and_save_acl_for(type:, resource:) + manager_class = manager_class_for(type) + return if manager_class.nil? + + manager_class.new(resource: resource).release! + resource.permission_manager.acl.save + resource + rescue Hyrax::EmbargoManager::NotReleasableError, Hyrax::LeaseManager::NotReleasableError + nil + end - item.copy_visibility_to_files unless item.is_a? FileSet + def manager_class_for(type) + case type + when :embargo + Hyrax::EmbargoManager + when :lease + Hyrax::LeaseManager + end + end - RegenerateThumbnailJob.perform_later(item) if regenerate_thumbnails == true + def copy_visibility_to_members!(resource:) + Hyrax.query_service.find_members(resource: resource).each do |member| + Hyrax::AccessControlList.copy_permissions(source: resource, target: member) end end end diff --git a/app/services/spot/exporters/work_members_exporter.rb b/app/services/spot/exporters/work_members_exporter.rb index 12de32ba9..fc37a73c3 100644 --- a/app/services/spot/exporters/work_members_exporter.rb +++ b/app/services/spot/exporters/work_members_exporter.rb @@ -3,6 +3,8 @@ # This name _might_ be misleading, I'm not 100% sure. For now, we're only # exporting a work's FileSets with this. Our parity release doesn't nest works, # but works can have multiple FileSets. +# +# @todo Update for Valkyrization module Spot module Exporters class WorkMembersExporter diff --git a/app/services/spot/exporters/work_metadata_exporter.rb b/app/services/spot/exporters/work_metadata_exporter.rb index 196bfc427..b4a83f512 100644 --- a/app/services/spot/exporters/work_metadata_exporter.rb +++ b/app/services/spot/exporters/work_metadata_exporter.rb @@ -1,26 +1,41 @@ # frozen_string_literal: true module Spot module Exporters + # Service to, well, export the metadata of a work. Requires a SolrDocument and an + # ActionDispatch::Request object to substitute out the local Fedora hostname with + # the site's (this is expected to be called from within a Controller context, see todo). + # Note that this is only used for Linked Data exports (:nt, :ttl, :jsonld) and not + # for :csv files. + # + # Supports the following Symbols for export formats: + # - :csv (comma-separated values) + # - :jsonld (json linked-data) + # - :nt (ntriples) + # - :ttl (turtle) + # - :all (all formats) + # + # @example + # solr_document = SolrDocument.find(id) + # export_destination = Rails.root.join('tmp', 'metadata_exports') + # FileUtils.mkdir_p(export_destination) unless Dir.exist?(export_destination) + # Spot::WorkMetadataExporter.new(solr_document).export!(destination: export_destination, format: :csv) + # + # @todo Hyrax::GraphExporter no longer requires a `request` parameter and now prefers a :hostname keyword param. + # We can substitute out "request" and use something like `ENV['APPLICATION_FQDN']` or + # `URI.parse(ENV['SITE_URL']).hostname` instead to make this independent of the controller context. class WorkMetadataExporter attr_reader :solr_document, :request # @param [SolrDocument] - # @param [Ability, nil] + # @param [ActionDispatch::Request, #host, nil] # @param [#host] - def initialize(solr_document, request = nil) + def initialize(solr_document) @solr_document = solr_document - @request = request end # @param [Pathname, String] :destination # Where to export the files # @param [Symbol] :format - # Format of exported metadata. Accepts: - # - :all (all formats) - # - :nt (ntriples) - # - :ttl (turtle) - # - :jsonld (json linked-data) - # - :csv (comma-separated values) # @return [void] def export!(destination:, format: :all) format = all_formats if format == :all @@ -55,12 +70,16 @@ def export_for_format(format) # @return [String] def generate_csv_content - Spot::WorkCSVService.new(solr_document).csv + Spot::WorkCsvService.new(solr_document).csv end # @return [RDF::Graph] def graph - @graph ||= Hyrax::GraphExporter.new(solr_document, request).fetch + @graph ||= graph_exporter.fetch + end + + def graph_exporter + Hyrax::GraphExporter.new(solr_document, hostname: URI.parse(ENV.fetch('URL_HOST')).hostname) end end end diff --git a/app/services/spot/exporters/zipped_work_exporter.rb b/app/services/spot/exporters/zipped_work_exporter.rb index 2b38caa5f..a34606654 100644 --- a/app/services/spot/exporters/zipped_work_exporter.rb +++ b/app/services/spot/exporters/zipped_work_exporter.rb @@ -4,14 +4,17 @@ module Spot module Exporters + # Helper class to wrap up metadata and file exports into a zip file. + # + # @see {Spot::Exporters::WorkMembersExporter} + # @see {Spot::Exporters::WorkMetadataExporter} class ZippedWorkExporter - attr_reader :solr_document, :request + attr_reader :solr_document # @param [SolrDocument] # @param [ActionDispatch::Request] - def initialize(solr_document, request) + def initialize(solr_document) @solr_document = solr_document - @request = request end # @param [Pathname, String] :destination @@ -71,7 +74,7 @@ def members_exporter # @return [Spot::Exporters::WorkMetadataExporter] def metadata_exporter - WorkMetadataExporter.new(solr_document, request) + WorkMetadataExporter.new(solr_document) end def zip_export_to(destination) diff --git a/app/services/spot/handle_service.rb b/app/services/spot/handle_service.rb index b764e1b2a..d98f4f0dc 100644 --- a/app/services/spot/handle_service.rb +++ b/app/services/spot/handle_service.rb @@ -40,23 +40,14 @@ def mint res = send_payload raise("Received error code minting handle [#{handle_id}]: #{res['responseCode']}") unless res['responseCode'] == 1 - return res['handle'] if work_has_handle? - work.identifier += [Spot::Identifier.new('hdl', res['handle']).to_s] - work.save! + update_work_identifier(handle: res['handle']) unless work_has_handle? res['handle'] end private - # @return [Faraday::Client] - def client - @client ||= - Faraday::Connection.new(self.class.handle_server_url, - ssl: { client_cert: handle_certificate, client_key: handle_key, verify: false }) - end - def find_handle_id stored = work.identifier.find { |id| id.start_with? 'hdl:' } return "#{self.class.handle_prefix}/#{work.id}" unless stored @@ -83,36 +74,29 @@ def handle_key OpenSSL::PKey.read(handle_key) end - # @return [String] - def payload - { - index: 100, - type: 'URL', - permissions: '1110', - data: { - format: 'string', - value: permalink_url - } - } - end - # @return [String] def permalink_url # need to use CGI.unescape as the slashes in our handle_id will be encoded by +handle_url+ CGI.unescape(Rails.application.routes.url_helpers.handle_url(handle_id, host: ENV['URL_HOST'])) end - # @param [Hash] options - # @option [Boolean] update_only # @return [void] # @todo update the record afterwards def send_payload - response = client.put do |req| - req.url "/api/handles/#{handle_id}" - req.headers['Content-Type'] = 'application/json' - req.headers['Authorization'] = 'Handle clientCert=true' - req.body = JSON.dump(payload) - end + payload = { + index: 100, + type: 'URL', + permissions: '1110', + data: { format: 'string', value: permalink_url } + } + + headers = { + 'Authorization' => 'Handle clientCert=true', + 'Content-Type' => 'application/json' + } + + put_url = "#{self.class.handle_server_url.gsub(/\/$/, '')}/api/handles/#{handle_id}" + response = Faraday.put(put_url, JSON.dump(payload), headers) # this isn't where we want to stop, we still need to # deal with the response: did everything go ok? @@ -120,6 +104,11 @@ def send_payload JSON.parse(response.body) end + def update_work_identifier(handle:) + work.identifier += [Spot::Identifier.new('hdl', handle).to_s] + Hyrax.persister.save(resource: work) + end + # @return [true, false] def work_has_handle? work.respond_to?(:identifier) && work.identifier.any? { |id| id.start_with? 'hdl:' } diff --git a/app/services/spot/iiif_service.rb b/app/services/spot/iiif_service.rb index 46fac8ee3..17a29628c 100644 --- a/app/services/spot/iiif_service.rb +++ b/app/services/spot/iiif_service.rb @@ -6,6 +6,8 @@ module Spot # Service for generating IIIF urls (via file_ids) for an external Cantaloupe image server. # Really, this could be used for _any_ external image server, save for the #download_url # method, which attaches the Cantaloupe-specific content-disposition query string. + # + # @todo Do we need to refactor download_url to work with Serverless IIIF? Valkyrization? class IiifService COMPLIANCE_LEVEL = 2 COMPLIANCE_LEVEL_URI = 'http://iiif.io/api/image/2/level2.json' @@ -97,6 +99,8 @@ def image_url(region: 'full', size: DEFAULT_SIZE, rotation: '0', quality: 'defau # @option [String] format (default: 'jpg') # @return [String] # @see https://cantaloupe-project.github.io/manual/4.1/endpoints.html#Response%20Content%20Disposition + # + # @todo does this work with us hosting IIIF assets on S3? def download_url(filename: nil, format: 'jpg', **args) filename = "#{file_set_id}.#{format}" if filename.nil? base_url = image_url(format: format, **args) diff --git a/app/services/spot/iso6391.rb b/app/services/spot/iso_6391.rb similarity index 58% rename from app/services/spot/iso6391.rb rename to app/services/spot/iso_6391.rb index 76543f1c6..f6ca1d832 100644 --- a/app/services/spot/iso6391.rb +++ b/app/services/spot/iso_6391.rb @@ -2,11 +2,11 @@ module Spot # Helper module for obtaining labels for ISO-639-1 values. Uses +I18n+ gem to allow # custom labels for languages to be provided as a locale file (see +config/locales/iso_639.en.yml+) - module ISO6391 + class Iso6391 # All of the ISO-639-1 entries in a key/val hash # # @example - # Spot::LanguageAuthority.all.first.to_h + # Spot::Iso6391.all.first.to_h # # => {'aa' => 'Afar'} # # @return [Array String>>] @@ -14,6 +14,23 @@ def self.all @all ||= ISO_639::ISO_639_1.select { |e| e.alpha2.present? }.map { |e| [e.alpha2, label_for(e.alpha2)] }.to_h end + # Wrapper method for fetching a label + # + # @example + # Spot::Iso6391.label_for('en') + # # => 'English' + # + # @return [String] + def self.label_for(id) + new(id).label + end + + # To make life a little easier for indexing, adding simple wrapper behavior that conforms to the + # pieces of Spot::ContorlledVocabularies::* that are used in the process. + def initialize(key) + @key = key.to_s.downcase + end + # Find the label for a language by its 2-char entry. # Possible values, in priority order, are: # @@ -23,9 +40,11 @@ def self.all # # @param [String] id # @return [String, NilClass] - def self.label_for(id) - id = id.to_s.downcase - I18n.t(id, scope: ['iso_639_1'], default: [ISO_639.find(id)&.english_name, id]) + def label + I18n.t(@key, scope: ['iso_639_1'], default: [ISO_639.find(@key)&.english_name, @key]) end + + # @see {Spot::RemoteLabelIndexing} + alias preferred_label label end end diff --git a/app/services/spot/listeners.rb b/app/services/spot/listeners.rb new file mode 100644 index 000000000..7753849a3 --- /dev/null +++ b/app/services/spot/listeners.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +module Spot + module Listeners + extend ActiveSupport::Autoload + + autoload :MintHandleListener + autoload :ParentCollectionMembershipListener + autoload :SolrSuggestDictionariesListener + end +end diff --git a/app/services/spot/listeners/mint_handle_listener.rb b/app/services/spot/listeners/mint_handle_listener.rb new file mode 100644 index 000000000..56747a7cf --- /dev/null +++ b/app/services/spot/listeners/mint_handle_listener.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +module Spot + module Listeners + # Enqueue MintHandleJob after a work is deposited. + class MintHandleListener + # @param [Dry::Events::Event] event + def on_object_deposited(event) + ::MintHandleJob.perform_later(event[:object].try(:id).try(:to_s)) + end + end + end +end diff --git a/app/services/spot/listeners/parent_collection_membership_listener.rb b/app/services/spot/listeners/parent_collection_membership_listener.rb new file mode 100644 index 000000000..f2320eeb0 --- /dev/null +++ b/app/services/spot/listeners/parent_collection_membership_listener.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +module Spot + module Listeners + # Listener to ensure that an object in a child collection is also included in the parent collection. + # Intended to replace Spot::Actors::CollectionsMembershipActor + class ParentCollectionMembershipListener + # @return [void] + def on_object_metadata_updated(event) + object = event[:object] + return if object.blank? || object.try(:member_of_collection_ids).blank? + + collection_ids_to_add = parent_collection_ids_for(object.member_of_collection_ids) + return if collection_ids_to_add.empty? + + object.member_of_collection_ids += collection_ids_to_add + Hyrax.persister.save(resource: object) + end + + private + + def parent_collection_ids_for(initial_collections) + collections_to_check = initial_collections.to_a + collection_ids_to_add = [] + + until collections_to_check.empty? + col_id = collections_to_check.shift + collection_ids_to_add << col_id + + collections_to_check += collection_ids_for(col_id) + collections_to_check.uniq! + end + + collection_ids_to_add - initial_collections + end + + def collection_ids_for(collection_id) + collection = Hyrax.query_service.find_by_alternate_identifier(alternate_identifier: collection_id) + collection.try(:member_of_collection_ids) || [] + end + end + end +end diff --git a/app/services/spot/listeners/solr_suggest_dictionaries_listener.rb b/app/services/spot/listeners/solr_suggest_dictionaries_listener.rb new file mode 100644 index 000000000..6a9888b73 --- /dev/null +++ b/app/services/spot/listeners/solr_suggest_dictionaries_listener.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +module Spot + module Listeners + # Listener replacement for SolrSuggestActor to update all of the + # Solr suggest dictionaries after a work's metadata is updated + # (these are used to power the autocomplete for fields like + # :keyword and :creator). + # + # Add an instance of this listener to the Hyrax::Publisher in + # the hyrax_events initializer. + # + # @example + # Hyrax.publisher.subscribe(Spot::Listeners::SolrSuggestDictionaryListener.new) + # + class SolrSuggestDictionariesListener + def on_object_metadata_updated(_event) + ::Spot::UpdateSolrSuggestDictionariesJob.perform_now + end + end + end +end diff --git a/app/services/spot/rdf_authority_parser.rb b/app/services/spot/rdf_authority_parser.rb index 46e9c5791..b89f95b60 100644 --- a/app/services/spot/rdf_authority_parser.rb +++ b/app/services/spot/rdf_authority_parser.rb @@ -9,14 +9,18 @@ # # @example # -# class FrenchRDFAuthorityParser < Spot::RDFAuthorityParser +# class FrenchRDFAuthorityParser < Spot::RdfAuthorityParser # self.preferred_language = :fr # end # -# FrenchRDFAuthorityParser.load_rdf('languages_fr', ['http://id.loc.gov/vocabulary/iso639-1.nt']) +# FrenchRdfAuthorityParser.load_rdf('languages_fr', ['http://id.loc.gov/vocabulary/iso639-1.nt']) +# +# @todo Okay to remove? I'm not entirely sure that we've ever used this? +# +# @todo Okay to remove? I'm not entirely sure that we've ever used this? # module Spot - class RDFAuthorityParser < ::Qa::Services::RDFAuthorityParser + class RdfAuthorityParser < ::Qa::Services::RDFAuthorityParser class_attribute :preferred_language self.preferred_language = :en diff --git a/app/services/spot/s3_path.rb b/app/services/spot/s3_path.rb new file mode 100644 index 000000000..733787036 --- /dev/null +++ b/app/services/spot/s3_path.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +module Spot + # Path generators for the Valkyrie storage adapters. The default Valkyrie-Shrine + # adapter combines the resource id with a generated uuid to prevent accidental + # overwriting of files, but we're not concerned with that w/r/t source objects + # for media playback; we just want the most recent derivative. + # + # This follows the API set with IdPathGenerator. As a hack, to generate a pathname + # from a resource without an original filename available (say you're trying to access + # an IIIF tif file but the original file is a jpg), passing a glob string to the + # :original_filename parameter will use whatever extension is provided. + # + # @example Delete an existing IIIF access copy for a file_set + # file_set = Hyrax.query_service.find_by_alternate_identifier(alternate_identifier: 'file_set__1') + # s3_identifier = Spot::S3Path::IiifPathGenerator.new.generate(resource: file_set, original_filename: '*.tif') + # adapter = Valkyrie::StorageAdapter.find(:iiif_source_s3) + # adapter.delete(id: s3_identifier) + # + # @see https://github.com/samvera-labs/valkyrie-shrine/blob/v1.0.0/lib/valkyrie/storage/shrine.rb#L14-L30 + # @see config/initializers/hyrax.rb + module S3Path + class Base + def initialize(base_path: nil); end + end + + # IIIF source images are created on disk before sending to S3, + # so the desired filename has already been generated. + class IiifPathGenerator < Base + def generate(original_filename:, **) + original_filename + end + end + + # @todo get this from Jenn's work + class AvPathGenerator < Base + def generate(resource:, file:, original_filename:); end + end + end +end diff --git a/app/services/spot/simple_schema_loader.rb b/app/services/spot/simple_schema_loader.rb new file mode 100644 index 000000000..889a8c782 --- /dev/null +++ b/app/services/spot/simple_schema_loader.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true +module Spot + # Using our own SchemaLoader for the time being to bypass an issue with the Hyrax loader + # where 'type: uri' and 'multiple: true' fields were passing the Dry::Struct interface default + # value of +Dry::Types::Unknown+ through, as it responds true to #present?, which was resulting + # in new resources having default URI values of 'Unknown' + # + # @example + # # config/metadata/cool_metadata.yml + # attributes: + # subject: + # type: uri + # multiple: true + # + # # app/models/cool_resource.rb + # class CoolResource < Hyrax::Resource + # include Hyrax::Schema(:cool_metadata) + # end + # + # CoolResource.new.subject + # => [#] + # + # # app/models/better_resource.rb + # class BetterResource < Hyrax::Resource + # include Hyrax::Schema(:cool_metadata, schema_loader: Spot::SimpleSchemaLoader.new) + # end + # + # BetterResource.new.subject + # => [] + # + class SimpleSchemaLoader < Hyrax::SimpleSchemaLoader + class AttributeDefinition < Hyrax::SimpleSchemaLoader::AttributeDefinition + def type + collection_type = if config['multiple'] + Valkyrie::Types::Array.constructor { |v| Array(v).select { |x| x.present? && x != Dry::Types::Undefined } } + else + Identity + end + collection_type.of(type_for(config['type'])) + end + end + + private + + # Map the definitions to use our overloaded AttributeDefinition class. + # + # @param [#to_s] schema_name + # @return [Enumerable 1 + record.errors.add(:date_issued, :invalid, message: 'Date Issued may not be blank') if record.date_issued.empty? + record.errors.add(:date_issued, :invalid, message: 'Date Issued may only contain one value') if record.date_issued.size > 1 record.date_issued.each do |date| - record.errors[:date_issued] << 'Date Issued must be in YYYY-MM-DD, YYYY-MM, or YYYY format' unless + record.errors.add(:date_issued, :invalid, message: 'Date Issued must be in YYYY-MM-DD, YYYY-MM, or YYYY format') unless date.match?(/\A\d{4}(-\d{2}){0,2}\z/) end end diff --git a/app/validators/spot/edtf_date_validator.rb b/app/validators/spot/edtf_date_validator.rb new file mode 100644 index 000000000..ef52ae640 --- /dev/null +++ b/app/validators/spot/edtf_date_validator.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +module Spot + class EdtfDateValidator < ::ActiveModel::Validator + def validate(record) + fields = Array.wrap(options[:fields] || options[:field]) + fields.each do |field| + Array.wrap(record.send(field)).compact.each do |value| + record.errors.add(field, :invalid, message: invalid_edtf_value_message(value)) if Date.edtf(value).nil? + end + end + end + + private + + def invalid_edtf_value_message(value) + "\"#{value}\" is not a valid EDTF date value." + end + end +end diff --git a/app/validators/spot/only_urls_validator.rb b/app/validators/spot/only_urls_validator.rb index d4ee56447..c161f6226 100644 --- a/app/validators/spot/only_urls_validator.rb +++ b/app/validators/spot/only_urls_validator.rb @@ -15,7 +15,7 @@ def validate(record) next if values.empty? values.each do |val| - record.errors[field] << "#{val} is not a valid URL" unless val.match?(uri_regex) + record.errors.add(field, :invalid, message: "#{val} is not a valid URL") unless val.match?(uri_regex) end end end diff --git a/app/validators/spot/required_local_authority_validator.rb b/app/validators/spot/required_local_authority_validator.rb index 0296f7898..1448b7eab 100644 --- a/app/validators/spot/required_local_authority_validator.rb +++ b/app/validators/spot/required_local_authority_validator.rb @@ -16,13 +16,12 @@ def validate(record) authority_name = options[:authority] field = options[:field] authority = authority_for(authority_name) - values = record.send(field) - values = Array.wrap(values) unless values.respond_to?(:each) + values = Array.wrap(values) unless values.class < Enumerable values.each do |v| value = v.is_a?(ActiveTriples::Resource) ? v.id : v.to_s - record.errors[field] << %("#{value}" is not a valid #{field.to_s.titleize}.) if authority.find(value).empty? + record.errors.add(field, :invalid, message: %("#{value}" is not a valid #{field.to_s.titleize}.)) if authority.find(value).empty? end end diff --git a/app/validators/spot/slug_validator.rb b/app/validators/spot/slug_validator.rb index aef053501..2d9da5633 100644 --- a/app/validators/spot/slug_validator.rb +++ b/app/validators/spot/slug_validator.rb @@ -13,8 +13,8 @@ def validate(record) slugs = record.send(field).select { |id| id.start_with? 'slug:' } next if slugs.empty? - record.errors[field] << single_slug_message unless slugs.size == 1 - record.errors[field] << slug_regex_message unless slug_valid?(slugs.first) + record.errors.add(field, :invalid, message: single_slug_message) unless slugs.size == 1 + record.errors.add(field, :invalid, message: slug_regex_message) unless slug_valid?(slugs.first) end end diff --git a/app/values/blacklight/types.rb b/app/values/blacklight/types.rb deleted file mode 100644 index b0751d1fb..000000000 --- a/app/values/blacklight/types.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true -# -# This is copied whole-hog from Blacklight@master and gives us types to use -# for -# @todo remove this file when Hyrax upgrades to Blacklight 7. -module Blacklight - # These are data types that blacklight can use to coerce values from the index - module Types - class Array - def self.coerce(input) - ::Array.wrap(input) - end - end - - class String - def self.coerce(input) - ::Array.wrap(input).first - end - end - - class Date - def self.coerce(input) - field = String.coerce(input) - return if field.blank? - - begin - ::Date.parse(field) - rescue ArgumentError - Rails.logger.info "Unable to parse date: #{field.first.inspect}" - end - end - end - end -end diff --git a/app/views/_controls.html.erb b/app/views/_controls.html.erb index a3695e84b..7bb5b5838 100644 --- a/app/views/_controls.html.erb +++ b/app/views/_controls.html.erb @@ -1,20 +1,16 @@ - diff --git a/app/views/_logo.html.erb b/app/views/_logo.html.erb index 072ae2139..5e50e583a 100644 --- a/app/views/_logo.html.erb +++ b/app/views/_logo.html.erb @@ -1,3 +1,3 @@ - diff --git a/app/views/_masthead.html.erb b/app/views/_masthead.html.erb deleted file mode 100644 index 83c8a9494..000000000 --- a/app/views/_masthead.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -
- -
diff --git a/app/views/_user_util_links.html.erb b/app/views/_user_util_links.html.erb deleted file mode 100644 index c2da425d3..000000000 --- a/app/views/_user_util_links.html.erb +++ /dev/null @@ -1,40 +0,0 @@ - diff --git a/app/views/catalog/_search_form.html.erb b/app/views/catalog/_search_form.html.erb index 531df7cb1..f5fbe3ed4 100644 --- a/app/views/catalog/_search_form.html.erb +++ b/app/views/catalog/_search_form.html.erb @@ -1,16 +1,16 @@ -<%= form_tag search_form_action, method: :get, class: "form-horizontal search-form", id: "search-form-header", role: "search" do %> - <%= render_hash_as_hidden_fields(search_state.params_for_search.except(:q, :search_field, :qt, :page, :utf8)) %> +<%= form_tag search_form_action, method: :get, class: "search-form", id: "search-form-header", role: "search" do %> + <%= render Blacklight::HiddenSearchStateComponent.new(params: search_state.params_for_search.except(:q, :search_field, :qt, :page, :utf8)) %> <%= hidden_field_tag :search_field, 'all_fields' %> -
+
-