diff --git a/.rubocop.yml b/.rubocop.yml index 0b886e33..288c681b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -669,6 +669,9 @@ RSpec/ExcessiveDocstringSpacing: RSpec/ExpectActual: Enabled: false +RSpec/ExpectChange: + Enabled: false + RSpec/ExpectInHook: Enabled: false diff --git a/Gemfile b/Gemfile index 57cb33f3..e49b3f27 100644 --- a/Gemfile +++ b/Gemfile @@ -5,10 +5,10 @@ source "https://rubygems.org" ruby "3.4.9" # stdlib -gem "csv", "~> 3.3.5" -gem "fiddle", "~> 1.1.8" +gem "csv", "~> 3.3" +gem "fiddle", "~> 1.1" gem "ostruct", "~> 0.6", require: false -gem "set", "~> 1.1.2" +gem "set", "~> 1.1" gem "sorted_set", "~> 1.1" gem "stringio", "~> 3.2" gem "syslog", "~> 0.4", require: false @@ -18,15 +18,15 @@ gem "activerecord", "~> 8.1" # Rails / database gem "rails", "~> 8.1.2" -gem "pg", "~> 1.6.2" -gem "activerecord-cte", "~> 0.4.0" +gem "pg", "~> 1.6" +gem "activerecord-cte", "~> 0.4" gem "active_record_distinct_on", "1.9.0" -gem "after_commit_everywhere", "~> 1.6.0" +gem "after_commit_everywhere", "~> 1.6" gem "async", "~> 2.34" gem "closure_tree", "~> 9.6" -gem "frozen_record", "~> 0.27.1" +gem "frozen_record", "~> 0.27" gem "ar_lazy_preload" -gem "pghero", "~> 3.7.0" +gem "pghero", "~> 3.8" gem "pg_query", "~> 6.2" gem "pg_search", "~> 2.3.7" gem "retryable", "~> 3.0.5" @@ -35,16 +35,16 @@ gem "store_model", "~> 4.5" # Redis / Jobs gem "good_job", "~> 4.13" -gem "redis", "~> 5.4.1" +gem "redis", "~> 5.4" gem "redis-objects", ">= 2.0" gem "job-iteration", "~> 1.12" # GraphQL gem "graphql", "~> 2.5" -gem "graphql-batch", "~> 0.6.1" -gem "graphql-client", "~> 0.26.0" -gem "graphql-fragment_cache", "~> 1.22.2" -gem "search_object_graphql", "~> 1.0.5", require: %w[search_object search_object/plugin/graphql] +gem "graphql-batch", "~> 0.6" +gem "graphql-client", "~> 0.26" +gem "graphql-fragment_cache", "~> 1.22" +gem "search_object_graphql", "~> 1.0", require: %w[search_object search_object/plugin/graphql] # dry-rb gem "dry-auto_inject", "~> 1.1" @@ -66,7 +66,7 @@ gem "dry-validation", "~> 1.11" # Keycloak / Auth gem "bcrypt", "~> 3.1" gem "keycloak-admin", "~> 1.1" -gem "keycloak_rack", "1.2.0" +gem "keycloak_rack", "1.2" # Metadata Parsing gem "lutaml-model", "~> 0.7" @@ -79,86 +79,86 @@ gem "action_policy", "~> 0.7" gem "action_policy-graphql", "~> 0.6" gem "acts_as_list", "~> 1.2.6" gem "addressable", "~> 2.8" -gem "ahoy_matey", "~> 5.4.1" -gem "anystyle", "~> 1.6.0" +gem "ahoy_matey", "~> 5.5" +gem "anystyle", "~> 1.6" gem "anyway_config", "~> 2.8" -gem "autotuner", "~> 1.1.0" +gem "autotuner", "~> 1.1" gem "connection_pool", "~> 3" -gem "down", "~> 5.4.2" -gem "faraday", "~> 2.14.1" +gem "down", "~> 5.4" +gem "faraday", "~> 2.14" gem "faraday-follow_redirects", "~> 0.5" gem "faraday-retry", "~> 2.4" -gem "ffi", "~> 1.17.2" -gem "fugit", "~> 1.12.1" -gem "geocoder", "~> 1.8.0" -gem "groupdate", "~> 6.7.0" -gem "hashids", "~> 1.0.6" -gem "htmlbeautifier", "~> 1.4.3" -gem "iso-639", "~> 0.3.8" -gem "jbuilder", "~> 2.14.1" +gem "ffi", "~> 1.17" +gem "fugit", "~> 1.12" +gem "geocoder", "~> 1.8" +gem "groupdate", "~> 6.7" +gem "hashids", "~> 1.0" +gem "htmlbeautifier", "~> 1.4" +gem "iso-639", "~> 0.3" +gem "jbuilder", "~> 2.14" gem "json_schemer", "~> 2.5" gem "jwt", "~> 2" -gem "kramdown", "~> 2.5.1" -gem "link-header-parser", "~> 7.0.1" +gem "kramdown", "~> 2.5" +gem "link-header-parser", "~> 7.0" gem "liquid", "~> 5.11" -gem "maxminddb", "~> 0.1.22" -gem "namae", "~> 1.2.0" +gem "maxminddb", "~> 0.1" +gem "namae", "~> 1.2" gem "naught", "~> 1.1.0" gem "nokogiri", "~> 1.19" -gem "oai", "~> 1.3.0" +gem "oai", "~> 1.3" gem "oj", "~> 3.16" -gem "openid_connect", "~> 2.3.0" -gem "ox", "~> 2.14.18" +gem "openid_connect", "~> 2.3" +gem "ox", "~> 2.14" gem "positioning", "~> 0.4" -gem "redcarpet", "~> 3.6.0" +gem "redcarpet", "~> 3.6" gem "reverse_markdown", "~> 3.0" -gem "ruby-limiter", "~> 2.3.0" -gem "sanitize", "~> 7.0.0" -gem "semantic", "~> 1.6.1" -gem "shale", "~> 1.2.1" -gem "sinatra", "~> 4.2.1", require: "sinatra/base" -gem "sinatra-contrib", "~> 4.2.1", require: false +gem "ruby-limiter", "~> 2.3" +gem "sanitize", "~> 7.0" +gem "semantic", "~> 1.6" +gem "shale", "~> 1.2" +gem "sinatra", "~> 4.2", require: "sinatra/base" +gem "sinatra-contrib", "~> 4.2", require: false gem "statesman", "~> 13.1" -gem "strip_attributes", "~> 2.0.0" -gem "tomlib", "~> 0.7.2" -gem "validate_url", "~> 1.0.15" +gem "strip_attributes", "~> 2.0" +gem "tomlib", "~> 0.7" +gem "validate_url", "~> 1.0" gem "with_advisory_lock", "~> 7.5" # File processing gem "aws-sdk-s3", "~> 1.214" -gem "content_disposition", "~> 1.0.0" -gem "image_processing", "~> 1.14.0" -gem "marcel", "~> 1.1.0" -gem "shrine", "~> 3.6.0", require: %w[shrine shrine/storage/s3 shrine/storage/memory shrine/storage/url] -gem "shrine-tus", "~> 2.1.1" -gem "shrine-url", "~> 2.4.1" -gem "mediainfo", "~> 1.5.0" -gem "tus-server", "~> 2.3.0", require: %w[tus/server tus/storage/s3] -gem "zaru", "~> 1.0.0" +gem "content_disposition", "~> 1.0" +gem "image_processing", "~> 1.14" +gem "marcel", "~> 1.1" +gem "shrine", "~> 3.6", require: %w[shrine shrine/storage/s3 shrine/storage/memory shrine/storage/url] +gem "shrine-tus", "~> 2.1" +gem "shrine-url", "~> 2.4" +gem "mediainfo", "~> 1.5" +gem "tus-server", "~> 2.3", require: %w[tus/server tus/storage/s3] +gem "zaru", "~> 1.0" # Servers / Rack -gem "pitchfork", "~> 0.18.1" -gem "rack", "~> 3.2.4" -gem "rack-cors", "~> 3.0.0" +gem "pitchfork", "~> 0.18" +gem "rack", "~> 3.2" +gem "rack-cors", "~> 3.0" # Debugging / system-level things gem "bootsnap", ">= 1.19.0", require: false gem "newrelic_rpm", "~> 10.0" -gem "pry-rails", "~> 0.3.11" +gem "pry-rails", "~> 0.3" gem "pry", "~> 0.16" gem "ruby-prof", "~> 2.0", require: false gem "stackprof", "~> 0.2.25", require: false group :development, :test do gem "derailed_benchmarks", require: false - gem "factory_bot_rails", "~> 6.5.0" + gem "factory_bot_rails", "~> 6.5" gem "faker", "~> 3.6" - gem "rspec", "~> 3.13.2" + gem "rspec", "~> 3.13" gem "rspec-rails", "~> 8.0" gem "vernier", require: false gem "yard", "~> 0.9.34", require: false - gem "yard-activerecord", "~> 0.0.16", require: false - gem "yard-activesupport-concern", "~> 0.0.1", require: false + gem "yard-activerecord", "~> 0.0", require: false + gem "yard-activesupport-concern", "~> 0.0", require: false end group :development do @@ -171,12 +171,12 @@ group :development do end group :test do - gem "database_cleaner-active_record", "~> 2.2.2" - gem "database_cleaner-redis", "~> 2.0.0" - gem "rspec-collection_matchers", "~> 1.2.0" - gem "rspec-json_expectations", "~> 2.2.0" - gem "simplecov", "~> 0.22.0", require: false - gem "test-prof", "~> 1.5.2" - gem "timecop", "~> 0.9.8" - gem "webmock", "3.26.1" + gem "database_cleaner-active_record", "~> 2.2" + gem "database_cleaner-redis", "~> 2.0" + gem "rspec-collection_matchers", "~> 1.2" + gem "rspec-json_expectations", "~> 2.2" + gem "simplecov", "~> 0.22", require: false + gem "test-prof", "~> 1.5" + gem "timecop", "~> 0.9" + gem "webmock", "~> 3.26" end diff --git a/Gemfile.lock b/Gemfile.lock index 5057f3bd..d5646406 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -18,31 +18,31 @@ GEM action_policy (~> 0.7) graphql (>= 1.9.3) ruby-next-core (~> 1.0) - action_text-trix (2.1.17) + action_text-trix (2.1.18) railties - actioncable (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) mail (>= 2.8.0) - actionmailer (8.1.2) - actionpack (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activesupport (= 8.1.2) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.1.2) - actionview (= 8.1.2) - activesupport (= 8.1.2) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -50,40 +50,40 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.1.2) + actiontext (8.1.3) action_text-trix (~> 2.1.15) - actionpack (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.1.2) - activesupport (= 8.1.2) + actionview (8.1.3) + activesupport (= 8.1.3) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) active_record_distinct_on (1.9.0) activerecord (>= 6.1) - activejob (8.1.2) - activesupport (= 8.1.2) + activejob (8.1.3) + activesupport (= 8.1.3) globalid (>= 0.3.6) - activemodel (8.1.2) - activesupport (= 8.1.2) - activerecord (8.1.2) - activemodel (= 8.1.2) - activesupport (= 8.1.2) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) timeout (>= 0.4.0) activerecord-cte (0.4.0) activerecord - activestorage (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activesupport (= 8.1.2) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) marcel (~> 1.0) - activesupport (8.1.2) + activesupport (8.1.3) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) @@ -99,14 +99,15 @@ GEM acts_as_list (1.2.6) activerecord (>= 6.1) activesupport (>= 6.1) - addressable (2.8.9) + addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) aes_key_wrap (1.1.0) after_commit_everywhere (1.6.0) activerecord (>= 4.2) activesupport - ahoy_matey (5.4.1) - activesupport (>= 7.1) + ahoy_matey (5.5.0) + activesupport (>= 7.2) + cgi device_detector (>= 1) safely_block (>= 0.4) anystyle (1.6.0) @@ -119,7 +120,7 @@ GEM ruby-next-core (~> 1.0) ar_lazy_preload (2.1.1) ast (2.4.3) - async (2.38.0) + async (2.39.0) console (~> 1.29) fiber-annotation io-event (~> 1.11) @@ -128,8 +129,8 @@ GEM attr_required (1.0.2) autotuner (1.1.0) aws-eventstream (1.4.0) - aws-partitions (1.1226.0) - aws-sdk-core (3.243.0) + aws-partitions (1.1241.0) + aws-sdk-core (3.246.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -137,27 +138,28 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.122.0) - aws-sdk-core (~> 3, >= 3.241.4) + aws-sdk-kms (1.124.0) + aws-sdk-core (~> 3, >= 3.244.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.216.0) - aws-sdk-core (~> 3, >= 3.243.0) + aws-sdk-s3 (1.220.0) + aws-sdk-core (~> 3, >= 3.244.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) base64 (0.3.0) - bcrypt (3.1.21) + bcrypt (3.1.22) benchmark-ips (2.14.0) bibtex-ruby (6.2.0) latex-decode (~> 0.0) logger (~> 1.7) racc (~> 1.7) - bigdecimal (4.0.1) + bigdecimal (4.1.2) bindata (2.5.1) - bootsnap (1.23.0) + bootsnap (1.24.0) msgpack (~> 1.2) builder (3.3.0) + cgi (0.5.1) closure_tree (9.6.1) activerecord (>= 7.2.0) with_advisory_lock (>= 7.5.0) @@ -205,8 +207,9 @@ GEM diff-lcs (1.6.2) docile (1.4.1) domain_name (0.6.20240107) - down (5.4.2) + down (5.6.0) addressable (~> 2.8) + base64 (~> 0.3) drb (2.2.3) dry-auto_inject (1.1.0) dry-core (~> 1.1) @@ -236,7 +239,7 @@ GEM zeitwerk (~> 2.6) dry-matcher (1.0.0) dry-core (~> 1.0, < 2) - dry-monads (1.9.0) + dry-monads (1.10.0) concurrent-ruby (~> 1.0) dry-core (~> 1.1) zeitwerk (~> 2.6) @@ -282,7 +285,7 @@ GEM activesupport (>= 3.0, < 9.0) email_validator (2.2.4) activemodel - erb (6.0.2) + erb (6.0.4) erubi (1.13.1) et-orbi (1.4.0) tzinfo @@ -291,7 +294,7 @@ GEM factory_bot_rails (6.5.1) factory_bot (~> 6.5) railties (>= 6.1.0) - faker (3.6.1) + faker (3.8.0) i18n (>= 1.8.11, < 2) faraday (2.14.1) faraday-net_http (>= 2.0, < 3.5) @@ -303,19 +306,16 @@ GEM net-http (~> 0.5) faraday-retry (2.4.0) faraday (~> 2.0) - ffi (1.17.3) - ffi (1.17.3-aarch64-linux-gnu) - ffi (1.17.3-aarch64-linux-musl) - ffi (1.17.3-arm-linux-gnu) - ffi (1.17.3-arm-linux-musl) - ffi (1.17.3-arm64-darwin) - ffi (1.17.3-x86-linux-gnu) - ffi (1.17.3-x86_64-darwin) - ffi (1.17.3-x86_64-linux-gnu) - ffi (1.17.3-x86_64-linux-musl) - ffi-compiler (1.3.2) - ffi (>= 1.15.5) - rake + ffi (1.17.4) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86-linux-gnu) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) fiber-annotation (0.2.0) fiber-local (1.1.0) fiber-storage @@ -334,38 +334,38 @@ GEM ffi (~> 1.0) globalid (1.3.0) activesupport (>= 6.1) - good_job (4.13.3) + good_job (4.18.2) activejob (>= 6.1.0) activerecord (>= 6.1.0) concurrent-ruby (>= 1.3.1) fugit (>= 1.11.0) railties (>= 6.1.0) thor (>= 1.0.0) - google-protobuf (4.34.0) + google-protobuf (4.34.1) bigdecimal rake (~> 13.3) - google-protobuf (4.34.0-aarch64-linux-gnu) + google-protobuf (4.34.1-aarch64-linux-gnu) bigdecimal rake (~> 13.3) - google-protobuf (4.34.0-aarch64-linux-musl) + google-protobuf (4.34.1-aarch64-linux-musl) bigdecimal rake (~> 13.3) - google-protobuf (4.34.0-arm64-darwin) + google-protobuf (4.34.1-arm64-darwin) bigdecimal rake (~> 13.3) - google-protobuf (4.34.0-x86-linux-gnu) + google-protobuf (4.34.1-x86-linux-gnu) bigdecimal rake (~> 13.3) - google-protobuf (4.34.0-x86_64-darwin) + google-protobuf (4.34.1-x86_64-darwin) bigdecimal rake (~> 13.3) - google-protobuf (4.34.0-x86_64-linux-gnu) + google-protobuf (4.34.1-x86_64-linux-gnu) bigdecimal rake (~> 13.3) - google-protobuf (4.34.0-x86_64-linux-musl) + google-protobuf (4.34.1-x86_64-linux-musl) bigdecimal rake (~> 13.3) - graphql (2.5.21) + graphql (2.6.1) base64 fiber-storage logger @@ -385,15 +385,12 @@ GEM heapy (0.2.0) thor htmlbeautifier (1.4.3) - http (5.3.1) - addressable (~> 2.8) + http (6.0.3) http-cookie (~> 1.0) - http-form_data (~> 2.2) - llhttp-ffi (~> 0.5.0) + llhttp (~> 0.6.1) http-accept (1.7.0) - http-cookie (1.1.0) + http-cookie (1.1.6) domain_name (~> 0.5) - http-form_data (2.3.0) i18n (1.14.8) concurrent-ruby (~> 1.0) ice_nine (0.11.2) @@ -401,8 +398,8 @@ GEM mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) io-console (0.8.2) - io-event (1.14.4) - irb (1.17.0) + io-event (1.15.1) + irb (1.18.0) pp (>= 0.6.0) prism (>= 1.3.0) rdoc (>= 4.0.0) @@ -413,9 +410,9 @@ GEM actionview (>= 7.0.0) activesupport (>= 7.0.0) jmespath (1.6.2) - job-iteration (1.12.0) - activejob (>= 6.1) - json (2.19.1) + job-iteration (1.13.0) + activejob (>= 7.0) + json (2.19.4) json-jwt (1.17.0) activesupport (>= 4.2) aes_key_wrap @@ -423,9 +420,6 @@ GEM bindata faraday (~> 2.0) faraday-follow_redirects - json-schema (6.2.0) - addressable (~> 2.8) - bigdecimal (>= 3.1, < 5) json_schemer (2.5.0) bigdecimal hana (~> 1.3) @@ -433,7 +427,7 @@ GEM simpleidn (~> 0.2) jwt (2.10.2) base64 - keycloak-admin (1.1.6) + keycloak-admin (1.1.7) http-cookie (~> 1.0, >= 1.0.3) rest-client (~> 2.0) keycloak_rack (1.2.0) @@ -458,16 +452,14 @@ GEM link-header-parser (7.0.1) addressable (~> 2.8) lint_roller (1.1.0) - liquid (5.11.0) + liquid (5.12.0) bigdecimal strscan (>= 3.1.1) listen (3.10.0) logger rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - llhttp-ffi (0.5.1) - ffi-compiler (~> 1.0) - rake (~> 13.0) + llhttp (0.6.1) logger (1.7.0) loofah (2.25.1) crass (~> 1.0.2) @@ -483,8 +475,6 @@ GEM net-smtp marcel (1.1.0) maxminddb (0.1.22) - mcp (0.8.0) - json-schema (>= 4.1) mediainfo (1.5.0) memory_profiler (1.1.0) method_source (1.1.0) @@ -492,13 +482,13 @@ GEM mime-types (3.7.0) logger mime-types-data (~> 3.2025, >= 3.2025.0507) - mime-types-data (3.2026.0303) + mime-types-data (3.2026.0414) mini_histogram (0.3.1) mini_magick (5.3.1) logger mini_mime (1.1.5) mini_portile2 (2.8.9) - minitest (6.0.2) + minitest (6.0.5) drb (~> 2.0) prism (~> 1.5) mods (3.0.5) @@ -507,18 +497,17 @@ GEM iso-639 nokogiri (>= 1.6.6) nom-xml (~> 1.0) - moxml (0.1.10) + moxml (0.1.18) msgpack (1.8.0) - multi_json (1.19.1) - mustermann (3.0.4) - ruby2_keywords (~> 0.0.1) + multi_json (1.20.1) + mustermann (3.1.1) mutex_m (0.3.0) namae (1.2.0) racc (~> 1.7) naught (1.1.0) net-http (0.9.1) uri (>= 0.11.1) - net-imap (0.6.3) + net-imap (0.6.4) date net-protocol net-pop (0.1.2) @@ -528,27 +517,27 @@ GEM net-smtp (0.5.1) net-protocol netrc (0.11.0) - newrelic_rpm (10.2.0) + newrelic_rpm (10.4.0) logger nio4r (2.7.5) - nokogiri (1.19.1) + nokogiri (1.19.3) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.19.1-aarch64-linux-gnu) + nokogiri (1.19.3-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.1-aarch64-linux-musl) + nokogiri (1.19.3-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.19.1-arm-linux-gnu) + nokogiri (1.19.3-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.19.1-arm-linux-musl) + nokogiri (1.19.3-arm-linux-musl) racc (~> 1.4) - nokogiri (1.19.1-arm64-darwin) + nokogiri (1.19.3-arm64-darwin) racc (~> 1.4) - nokogiri (1.19.1-x86_64-darwin) + nokogiri (1.19.3-x86_64-darwin) racc (~> 1.4) - nokogiri (1.19.1-x86_64-linux-gnu) + nokogiri (1.19.3-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.1-x86_64-linux-musl) + nokogiri (1.19.3-x86_64-linux-musl) racc (~> 1.4) nom-xml (1.2.0) i18n @@ -558,7 +547,7 @@ GEM faraday (< 3) faraday-follow_redirects (>= 0.3.0, < 2) rexml - oj (3.16.16) + oj (3.17.0) bigdecimal (>= 3.0) ostruct (>= 0.2) openid_connect (2.3.1) @@ -575,10 +564,10 @@ GEM validate_url webfinger (~> 2.0) ostruct (0.6.3) - ox (2.14.23) + ox (2.14.25) bigdecimal (>= 3.0) - parallel (1.27.0) - parser (3.3.10.2) + parallel (2.1.0) + parser (3.3.11.1) ast (~> 2.4.1) racc pg (1.6.3) @@ -593,12 +582,12 @@ GEM pg_search (2.3.7) activerecord (>= 6.1) activesupport (>= 6.1) - pghero (3.7.0) - activerecord (>= 7.1) + pghero (3.8.0) + activerecord (>= 7.2) pitchfork (0.18.2) logger rack (>= 2.0) - positioning (0.4.7) + positioning (0.4.8) activerecord (>= 6.1) activesupport (>= 6.1) pp (0.6.3) @@ -618,7 +607,7 @@ GEM public_suffix (7.0.5) raabro (1.4.0) racc (1.8.1) - rack (3.2.5) + rack (3.2.6) rack-cors (3.0.0) logger rack (>= 3.0.14) @@ -633,27 +622,27 @@ GEM base64 (>= 0.1.0) logger (>= 1.6.0) rack (>= 3.0.0, < 4) - rack-session (2.1.1) + rack-session (2.1.2) base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.2.0) rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (8.1.2) - actioncable (= 8.1.2) - actionmailbox (= 8.1.2) - actionmailer (= 8.1.2) - actionpack (= 8.1.2) - actiontext (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activemodel (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) bundler (>= 1.15.0) - railties (= 8.1.2) + railties (= 8.1.3) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -661,9 +650,9 @@ GEM rails-html-sanitizer (1.7.0) loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -671,7 +660,7 @@ GEM tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.3.1) + rake (13.4.2) rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) @@ -683,11 +672,11 @@ GEM redcarpet (3.6.1) redis (5.4.1) redis-client (>= 0.22.0) - redis-client (0.27.0) + redis-client (0.28.0) connection_pool redis-objects (2.0.0) redis (~> 5.0) - regexp_parser (2.11.3) + regexp_parser (2.12.0) reline (0.6.3) io-console (~> 0.5) rest-client (2.1.0) @@ -699,7 +688,7 @@ GEM reverse_markdown (3.0.2) nokogiri rexml (3.4.4) - roda (3.102.0) + roda (3.103.0) rack rspec (3.13.2) rspec-core (~> 3.13.0) @@ -725,12 +714,11 @@ GEM rspec-mocks (>= 3.13.0, < 5.0.0) rspec-support (>= 3.13.0, < 5.0.0) rspec-support (3.13.7) - rubocop (1.85.1) + rubocop (1.86.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) - mcp (~> 0.6) - parallel (~> 1.10) + parallel (>= 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) @@ -762,12 +750,12 @@ GEM base64 ostruct ruby-progressbar (1.13.0) - ruby-statistics (4.1.0) + ruby-statistics (4.1.1) ruby-vips (2.3.0) ffi (~> 1.12) logger ruby2_keywords (0.0.5) - safely_block (0.5.0) + safely_block (1.0.0) sanitize (7.0.0) crass (~> 1.0.2) nokogiri (>= 1.16.8) @@ -780,7 +768,7 @@ GEM search_object (~> 1.2.5) securerandom (0.4.1) semantic (1.6.1) - set (1.1.2) + set (1.1.3) shale (1.2.2) bigdecimal shrine (3.6.0) @@ -790,10 +778,10 @@ GEM shrine (~> 3.2) shrine-url (~> 2.4) tus-server (~> 2.0) - shrine-url (2.4.1) + shrine-url (2.4.2) down (~> 5.0) - http (>= 3.2, < 6) - shrine (>= 3.0.0.rc, < 4) + http (>= 3.2) + shrine (~> 3.0) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -823,7 +811,7 @@ GEM stringio (3.2.0) strip_attributes (2.0.1) activemodel (>= 3.0, < 9.0) - strscan (3.1.7) + strscan (3.1.8) swd (2.0.3) activesupport (>= 3) attr_required (>= 0.0.5) @@ -831,10 +819,10 @@ GEM faraday-follow_redirects syslog (0.4.0) logger - test-prof (1.5.2) + test-prof (1.6.1) thor (1.5.0) tilt (2.7.0) - timecop (0.9.10) + timecop (0.9.11) timeout (0.6.1) tomlib (0.7.3) bigdecimal @@ -861,7 +849,7 @@ GEM activesupport faraday (~> 2.0) faraday-follow_redirects - webmock (3.26.1) + webmock (3.26.2) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -872,7 +860,7 @@ GEM with_advisory_lock (7.5.0) activerecord (>= 7.2) zeitwerk (>= 2.7) - yard (0.9.38) + yard (0.9.43) yard-activerecord (0.0.17) activesupport yard (>= 0.8.3) @@ -901,28 +889,28 @@ DEPENDENCIES action_policy-graphql (~> 0.6) active_record_distinct_on (= 1.9.0) activerecord (~> 8.1) - activerecord-cte (~> 0.4.0) + activerecord-cte (~> 0.4) activesupport (~> 8.1) acts_as_list (~> 1.2.6) addressable (~> 2.8) - after_commit_everywhere (~> 1.6.0) - ahoy_matey (~> 5.4.1) - anystyle (~> 1.6.0) + after_commit_everywhere (~> 1.6) + ahoy_matey (~> 5.5) + anystyle (~> 1.6) anyway_config (~> 2.8) ar_lazy_preload async (~> 2.34) - autotuner (~> 1.1.0) + autotuner (~> 1.1) aws-sdk-s3 (~> 1.214) bcrypt (~> 3.1) bootsnap (>= 1.19.0) closure_tree (~> 9.6) connection_pool (~> 3) - content_disposition (~> 1.0.0) - csv (~> 3.3.5) - database_cleaner-active_record (~> 2.2.2) - database_cleaner-redis (~> 2.0.0) + content_disposition (~> 1.0) + csv (~> 3.3) + database_cleaner-active_record (~> 2.2) + database_cleaner-redis (~> 2.0) derailed_benchmarks - down (~> 5.4.2) + down (~> 5.4) dry-auto_inject (~> 1.1) dry-core (~> 1.2) dry-effects (~> 0.5) @@ -938,112 +926,112 @@ DEPENDENCIES dry-transformer (~> 1.0) dry-types (~> 1.9) dry-validation (~> 1.11) - factory_bot_rails (~> 6.5.0) + factory_bot_rails (~> 6.5) faker (~> 3.6) - faraday (~> 2.14.1) + faraday (~> 2.14) faraday-follow_redirects (~> 0.5) faraday-retry (~> 2.4) - ffi (~> 1.17.2) - fiddle (~> 1.1.8) - frozen_record (~> 0.27.1) - fugit (~> 1.12.1) - geocoder (~> 1.8.0) + ffi (~> 1.17) + fiddle (~> 1.1) + frozen_record (~> 0.27) + fugit (~> 1.12) + geocoder (~> 1.8) good_job (~> 4.13) graphql (~> 2.5) - graphql-batch (~> 0.6.1) - graphql-client (~> 0.26.0) - graphql-fragment_cache (~> 1.22.2) - groupdate (~> 6.7.0) - hashids (~> 1.0.6) - htmlbeautifier (~> 1.4.3) - image_processing (~> 1.14.0) - iso-639 (~> 0.3.8) - jbuilder (~> 2.14.1) + graphql-batch (~> 0.6) + graphql-client (~> 0.26) + graphql-fragment_cache (~> 1.22) + groupdate (~> 6.7) + hashids (~> 1.0) + htmlbeautifier (~> 1.4) + image_processing (~> 1.14) + iso-639 (~> 0.3) + jbuilder (~> 2.14) job-iteration (~> 1.12) json_schemer (~> 2.5) jwt (~> 2) keycloak-admin (~> 1.1) - keycloak_rack (= 1.2.0) - kramdown (~> 2.5.1) - link-header-parser (~> 7.0.1) + keycloak_rack (= 1.2) + kramdown (~> 2.5) + link-header-parser (~> 7.0) liquid (~> 5.11) listen (~> 3.10) lutaml-model (~> 0.7) - marcel (~> 1.1.0) - maxminddb (~> 0.1.22) - mediainfo (~> 1.5.0) + marcel (~> 1.1) + maxminddb (~> 0.1) + mediainfo (~> 1.5) mods (~> 3.0) - namae (~> 1.2.0) + namae (~> 1.2) naught (~> 1.1.0) newrelic_rpm (~> 10.0) niso-jats! nokogiri (~> 1.19) - oai (~> 1.3.0) + oai (~> 1.3) oj (~> 3.16) - openid_connect (~> 2.3.0) + openid_connect (~> 2.3) ostruct (~> 0.6) - ox (~> 2.14.18) - pg (~> 1.6.2) + ox (~> 2.14) + pg (~> 1.6) pg_query (~> 6.2) pg_search (~> 2.3.7) - pghero (~> 3.7.0) - pitchfork (~> 0.18.1) + pghero (~> 3.8) + pitchfork (~> 0.18) positioning (~> 0.4) pry (~> 0.16) - pry-rails (~> 0.3.11) - rack (~> 3.2.4) - rack-cors (~> 3.0.0) + pry-rails (~> 0.3) + rack (~> 3.2) + rack-cors (~> 3.0) rails (~> 8.1.2) - redcarpet (~> 3.6.0) - redis (~> 5.4.1) + redcarpet (~> 3.6) + redis (~> 5.4) redis-objects (>= 2.0) retryable (~> 3.0.5) reverse_markdown (~> 3.0) - rspec (~> 3.13.2) - rspec-collection_matchers (~> 1.2.0) - rspec-json_expectations (~> 2.2.0) + rspec (~> 3.13) + rspec-collection_matchers (~> 1.2) + rspec-json_expectations (~> 2.2) rspec-rails (~> 8.0) rubocop (~> 1.85) rubocop-factory_bot (~> 2.28) rubocop-rails (~> 2.34) rubocop-rspec (~> 3.9) rubocop-rspec_rails (~> 2.32) - ruby-limiter (~> 2.3.0) + ruby-limiter (~> 2.3) ruby-prof (~> 2.0) - sanitize (~> 7.0.0) + sanitize (~> 7.0) scenic (~> 1.9.0) - search_object_graphql (~> 1.0.5) - semantic (~> 1.6.1) - set (~> 1.1.2) - shale (~> 1.2.1) - shrine (~> 3.6.0) - shrine-tus (~> 2.1.1) - shrine-url (~> 2.4.1) - simplecov (~> 0.22.0) - sinatra (~> 4.2.1) - sinatra-contrib (~> 4.2.1) + search_object_graphql (~> 1.0) + semantic (~> 1.6) + set (~> 1.1) + shale (~> 1.2) + shrine (~> 3.6) + shrine-tus (~> 2.1) + shrine-url (~> 2.4) + simplecov (~> 0.22) + sinatra (~> 4.2) + sinatra-contrib (~> 4.2) sorted_set (~> 1.1) stackprof (~> 0.2.25) statesman (~> 13.1) store_model (~> 4.5) stringio (~> 3.2) - strip_attributes (~> 2.0.0) + strip_attributes (~> 2.0) syslog (~> 0.4) - test-prof (~> 1.5.2) - timecop (~> 0.9.8) - tomlib (~> 0.7.2) - tus-server (~> 2.3.0) - validate_url (~> 1.0.15) + test-prof (~> 1.5) + timecop (~> 0.9) + tomlib (~> 0.7) + tus-server (~> 2.3) + validate_url (~> 1.0) vernier - webmock (= 3.26.1) + webmock (~> 3.26) with_advisory_lock (~> 7.5) yard (~> 0.9.34) - yard-activerecord (~> 0.0.16) - yard-activesupport-concern (~> 0.0.1) - zaru (~> 1.0.0) + yard-activerecord (~> 0.0) + yard-activesupport-concern (~> 0.0) + zaru (~> 1.0) RUBY VERSION ruby 3.4.9 BUNDLED WITH - 4.0.8 + 4.0.9 diff --git a/app/controllers/downloads_controller.rb b/app/controllers/downloads_controller.rb index 08b3fa4c..49051b06 100644 --- a/app/controllers/downloads_controller.rb +++ b/app/controllers/downloads_controller.rb @@ -2,11 +2,13 @@ class DownloadsController < ApplicationController def show - asset = Asset.find_graphql_slug params[:id] + asset = subject = Asset.find_graphql_slug params[:id] + + entity = asset.attachable perform_operation "assets.decode_download_token", asset, params[:token] do |m| - m.success do - ahoy.track "asset.download", subject: asset, entity: asset.attachable + m.success do |mode| + ahoy.track("asset.#{mode}", subject:, entity:) redirect_to asset.actual_download_url, status: :see_other, allow_other_host: true end diff --git a/app/graphql/mutations/contributor_user_link_destroy.rb b/app/graphql/mutations/contributor_user_link_destroy.rb new file mode 100644 index 00000000..e99b5546 --- /dev/null +++ b/app/graphql/mutations/contributor_user_link_destroy.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Mutations + # @see Mutations::Operations::ContributorUserLinkDestroy + class ContributorUserLinkDestroy < Mutations::BaseMutation + description <<~TEXT + Destroy a single `ContributorUserLink` record. + TEXT + + argument :contributor_user_link_id, ID, loads: Types::ContributorUserLinkType, required: true do + description <<~TEXT + The contributor user link to destroy. + TEXT + end + + performs_operation! "mutations.operations.contributor_user_link_destroy", destroy: true + end +end diff --git a/app/graphql/mutations/contributor_user_link_upsert.rb b/app/graphql/mutations/contributor_user_link_upsert.rb new file mode 100644 index 00000000..630e96fc --- /dev/null +++ b/app/graphql/mutations/contributor_user_link_upsert.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Mutations + # @see Mutations::Operations::ContributorUserLinkUpsert + class ContributorUserLinkUpsert < Mutations::BaseMutation + description <<~TEXT + Create or update a link between a `Contributor` and a `User`. + TEXT + + field :contributor, Types::ContributorType, null: true do + description <<~TEXT + The newly-linked contributor, if successful. + TEXT + end + + field :contributor_user_link, Types::ContributorUserLinkType, null: true do + description <<~TEXT + The newly-created or updated link, if successful. + TEXT + end + + field :user, Types::UserType, null: true do + description <<~TEXT + The user linked to the contributor, if successful. + TEXT + end + + argument :contributor_id, ID, loads: Types::ContributorType, required: true do + description <<~TEXT + The contributor to update. + TEXT + end + + argument :user_id, ID, loads: Types::UserType, required: true do + description <<~TEXT + The user to link to the contributor. + TEXT + end + + argument :linkage, Types::ContributorUserLinkageType, required: true do + description <<~TEXT + The type of link to create or update between the contributor and user. + + Setting `PRIMARY` will override any other primary set for the associated user. + TEXT + end + + performs_operation! "mutations.operations.contributor_user_link_upsert" + end +end diff --git a/app/graphql/mutations/submission_create.rb b/app/graphql/mutations/submission_create.rb index a483c3a5..d2598f2c 100644 --- a/app/graphql/mutations/submission_create.rb +++ b/app/graphql/mutations/submission_create.rb @@ -33,6 +33,14 @@ class SubmissionCreate < Mutations::BaseMutation TEXT end + argument :agreement_accepted, Boolean, required: false, default_value: false, replace_null_with_default: true do + description <<~TEXT + Whether or not the submitter has accepted the agreement for this submission. + + This must be true. + TEXT + end + argument :title, String, required: true do description <<~TEXT The title of the submission. diff --git a/app/graphql/mutations/submission_target_configure.rb b/app/graphql/mutations/submission_target_configure.rb index 8d0d5f3e..46541798 100644 --- a/app/graphql/mutations/submission_target_configure.rb +++ b/app/graphql/mutations/submission_target_configure.rb @@ -62,6 +62,12 @@ class SubmissionTargetConfigure < Mutations::BaseMutation TEXT end + argument :auto_approve_depositors, Boolean, required: false, default_value: false, replace_null_with_default: true do + description <<~TEXT + Whether depositors should be automatically approved when they request to become a depositor for this submission target. + TEXT + end + argument :description, Types::SubmissionTargetDescriptionInputType, required: true do description <<~TEXT A description of this submission target, which may be displayed to submitters when making a submission to this target. diff --git a/app/graphql/types/contributor_base_type.rb b/app/graphql/types/contributor_base_type.rb index ec29d63e..b3681f29 100644 --- a/app/graphql/types/contributor_base_type.rb +++ b/app/graphql/types/contributor_base_type.rb @@ -101,6 +101,14 @@ module ContributorBaseType TEXT end + field :user_link, Types::ContributorUserLinkType, null: true do + description <<~TEXT + The link between this contributor and a user, if any exists. + TEXT + end + + load_association! :contributor_user_link, as: :user_link + # @return [] def links Array(object.links).compact diff --git a/app/graphql/types/contributor_user_link_type.rb b/app/graphql/types/contributor_user_link_type.rb new file mode 100644 index 00000000..747536df --- /dev/null +++ b/app/graphql/types/contributor_user_link_type.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Types + # @see ContributorUserLink + class ContributorUserLinkType < Types::AbstractModel + description <<~TEXT + A link between a `Contributor` and a `User`, indicating that the user + is represented within Meru as that record. + TEXT + + field :contributor, "::Types::ContributorType", null: false do + description <<~TEXT + The contributor associated with this link. + TEXT + end + + field :user, "::Types::UserType", null: true do + description <<~TEXT + The user associated with this link. + + If the current viewer cannot see the user record, this will be null. + TEXT + end + + field :linkage, Types::ContributorUserLinkageType, null: false do + description <<~TEXT + The type of link between the contributor and user. + TEXT + end + + load_association! :contributor + load_association! :user + end +end diff --git a/app/graphql/types/contributor_user_linkage_type.rb b/app/graphql/types/contributor_user_linkage_type.rb new file mode 100644 index 00000000..bfeb7111 --- /dev/null +++ b/app/graphql/types/contributor_user_linkage_type.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Types + # @see ContributorUserLink + class ContributorUserLinkageType < Types::BaseEnum + description <<~TEXT + The type of link between a `Contributor` and a `User` in a `ContributorUserLink`. + TEXT + + value "PRIMARY", value: "primary" do + description <<~TEXT + A primary link indicates that the `Contributor` is the primary identity for the user. + + A user may only have one primary link. Setting a new primary link will invalidate the previous. + TEXT + end + + value "AUXILIARY", value: "auxiliary" do + description <<~TEXT + An auxiliary link indicates that the `Contributor` is an additional identity for the user. + + This may be the case for users who've published under different names, or are being managed + by a user account representing a team or organization, etc. + + A user may have multiple auxiliary links. Setting a new auxiliary link does not affect existing ones. + TEXT + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 8e72165a..1881d240 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -177,5 +177,9 @@ class MutationType < Types::BaseObject field :upsert_contribution, mutation: Mutations::UpsertContribution field :user_reset_password, mutation: Mutations::UserResetPassword + + field :contributor_user_link_destroy, mutation: Mutations::ContributorUserLinkDestroy + + field :contributor_user_link_upsert, mutation: Mutations::ContributorUserLinkUpsert end end diff --git a/app/graphql/types/schema_version_type.rb b/app/graphql/types/schema_version_type.rb index 21f643b1..274635b7 100644 --- a/app/graphql/types/schema_version_type.rb +++ b/app/graphql/types/schema_version_type.rb @@ -57,6 +57,12 @@ class SchemaVersionType < Types::AbstractModel TEXT end + field :submittable_versions, [Types::SchemaVersionType, { null: false }], null: false do + description <<~TEXT + The versions that are allowed to be submitted to this schema. + TEXT + end + field :enforced_child_declarations, [Types::SlugType, { null: false }], null: false do description <<~TEXT Declarations / slugs for `enforcedChildVersions`. @@ -91,6 +97,8 @@ class SchemaVersionType < Types::AbstractModel load_association! :enforced_child_versions + load_association! :submittable_versions + # @see Schemas::Versions::Configuration#render # @return [Schemas::Versions::RenderDefinition] def render diff --git a/app/graphql/types/schematic/scalar_property_type.rb b/app/graphql/types/schematic/scalar_property_type.rb index c96a8fd9..008fde2f 100644 --- a/app/graphql/types/schematic/scalar_property_type.rb +++ b/app/graphql/types/schematic/scalar_property_type.rb @@ -13,6 +13,12 @@ module ScalarPropertyType TEXT end + field :instructions, String, null: true do + description <<~TEXT + Instructions for filling out this property during submission, if applicable. + TEXT + end + field :required, Boolean, null: false do description <<~TEXT Whether or not this property is required in order for the schema instance @@ -24,6 +30,12 @@ module ScalarPropertyType TEXT end + field :submittable, Boolean, null: false do + description <<~TEXT + Whether or not this property should be displayed as part of the submission process for this schema. + TEXT + end + field :function, Types::SchemaPropertyFunctionType, null: false do description <<~TEXT The purpose or intent of this property relative to its entity, parents, and others. diff --git a/app/graphql/types/submission_target_type.rb b/app/graphql/types/submission_target_type.rb index 21565384..f9eef313 100644 --- a/app/graphql/types/submission_target_type.rb +++ b/app/graphql/types/submission_target_type.rb @@ -66,6 +66,12 @@ class SubmissionTargetType < Types::AbstractModel TEXT end + field :auto_approve_depositors, Boolean, null: false do + description <<~TEXT + Whether or not depositors should be automatically approved when they request to become a depositor for this submission target. + TEXT + end + field :description, ::Types::SubmissionTargetDescriptionType, null: false do description <<~TEXT A description of this submission target, which may include a human-readable title and/or a machine-readable schema.org description. diff --git a/app/graphql/types/submission_type.rb b/app/graphql/types/submission_type.rb index 21cceac4..5024b107 100644 --- a/app/graphql/types/submission_type.rb +++ b/app/graphql/types/submission_type.rb @@ -59,6 +59,12 @@ class SubmissionType < Types::AbstractModel TEXT end + field :agreement_accepted_at, ::GraphQL::Types::ISO8601DateTime, null: true do + description <<~TEXT + The timestamp of when the submitter accepted the agreement for this submission, if applicable. + TEXT + end + expose_authorization_rule :alter_schema_version?, <<~TEXT Whether or not the current user can alter the schema version of this submission. TEXT diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb index f8b13b5e..48f68213 100644 --- a/app/graphql/types/user_type.rb +++ b/app/graphql/types/user_type.rb @@ -83,6 +83,20 @@ class UserType < Types::AbstractModel description "All access grants for this user on an item" end + field :contributor_links, [::Types::ContributorUserLinkType, { null: false }], null: false do + description <<~TEXT + Any links between this user and contributors in the system. + TEXT + end + + field :primary_contributor, Types::ContributorType, null: true do + description <<~TEXT + The primary contributor associated with this user, if any. + + For the actual link records, see `User.contributorLinks`. + TEXT + end + expose_authorization_rule :receive_review_requests?, <<~TEXT Whether this user is a reviewer on **any** submission targets, and should see information about potential review requests in the UI. @@ -98,6 +112,9 @@ class UserType < Types::AbstractModel Whether this user has permission to trigger revalidation of the entire frontend. TEXT + load_association! :primary_contributor + load_association! :user_contributor_links, as: :contributor_links + # @see AnonymousInterface#system_slug_id # @see User#system_slug_id # @return [String] diff --git a/app/models/asset.rb b/app/models/asset.rb index 5ae88e48..208f2fdb 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -42,10 +42,8 @@ class Asset < ApplicationRecord delegate :original_filename, to: :attachment, allow_nil: true - # Compatibility for {EntityChildRecordPolicy} - def entity - attachable - end + # @note Compatibility for {EntityChildRecordPolicy} + def entity = attachable # We mask this with {#download_url} in order to track analytics of a download. # @@ -54,14 +52,14 @@ def actual_download_url attachment.url( public: Rails.env.development?, expires_in: 15.minutes.to_i, - response_content_disposition: content_disposition, + response_content_disposition:, ) end # @return [String] - def content_disposition - ContentDisposition.attachment(download_name) - end + def content_disposition = ContentDisposition.attachment(download_name) + + alias response_content_disposition content_disposition # @return [String] def download_name @@ -90,13 +88,12 @@ def download_token end # @return [String] - def download_url - generate_download_url! - end + def download_url = generate_download_url!(mode: "download") - def has_attachment? - persisted? && attachment.present? - end + # @return [String] + def view_url = generate_download_url!(mode: "view") + + def has_attachment? = persisted? && attachment.present? # @return [Class] def graphql_node_type @@ -124,13 +121,9 @@ def refresh_metadata! # @!attribute [r] signature # @return [String, nil] - def signature - attachment&.metadata&.[]("sha256") - end + def signature = attachment&.metadata&.[]("sha256") - def to_schematic_referent_label - name - end + def to_schematic_referent_label = name # @param [String] token # @return [Dry::Monads::Success(Boolean)] diff --git a/app/models/community.rb b/app/models/community.rb index 0bae39f3..f588c198 100644 --- a/app/models/community.rb +++ b/app/models/community.rb @@ -64,4 +64,10 @@ def hierarchical_parent = nil monadic_operation! def purge(...) call_operation("communities.purge", self, ...) end + + class << self + # @note compatibility with {Submittable} implementations + # @return [ActiveRecord::Relation] + def sans_drafts = all + end end diff --git a/app/models/concerns/submittable.rb b/app/models/concerns/submittable.rb index 1a689d6f..29c5710f 100644 --- a/app/models/concerns/submittable.rb +++ b/app/models/concerns/submittable.rb @@ -18,6 +18,11 @@ module Submittable has_one :submission, as: :entity, dependent: :nullify, inverse_of: :entity has_one :submitter, through: :submission, source: :user + + # Used for default filtering of items. + scope :sans_drafts, -> { not_submission_draft } + + before_validation :enforce_hidden_if_draft! end # @see SubmissionTargets::Configure @@ -33,4 +38,11 @@ module Submittable monadic_operation! def fetch_submission_target call_operation("submission_targets.fetch", self) end + + private + + # @return [void] + def enforce_hidden_if_draft! + self.visibility = :hidden if submission_draft? + end end diff --git a/app/models/contributor.rb b/app/models/contributor.rb index 9d767638..6bac6138 100644 --- a/app/models/contributor.rb +++ b/app/models/contributor.rb @@ -27,6 +27,10 @@ class Contributor < ApplicationRecord has_many :item_contributions, dependent: :destroy, inverse_of: :contributor has_many :items, through: :item_contributions + has_one :contributor_user_link, dependent: :destroy, inverse_of: :contributor + + has_one :user, through: :contributor_user_link + scope :by_kind, ->(kind) { where(kind:) } scope :by_orcid, ->(orcid) { where(orcid:) } scope :unharvested, -> { where.not(id: HarvestContributor.harvested_ids) } diff --git a/app/models/contributor_user_link.rb b/app/models/contributor_user_link.rb new file mode 100644 index 00000000..7d8b1ee0 --- /dev/null +++ b/app/models/contributor_user_link.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# A connection between a {Contributor} and a {User}, indicating that the user is +# represented within Meru as that record. +class ContributorUserLink < ApplicationRecord + include HasEphemeralSystemSlug + include TimestampScopes + + pg_enum! :linkage, as: :contributor_user_linkage, suffix: :linkage + + belongs_to :contributor, inverse_of: :contributor_user_link + + belongs_to :user, inverse_of: :contributor_user_links + + scope :in_default_order, -> { order(linkage: :asc).joins(:contributor).merge(Contributor.in_default_order) } + + before_save :enforce_primary_uniqueness!, if: :primary_linkage? + + private + + # @return [void] + def enforce_primary_uniqueness! + self.class.where(user_id:).primary_linkage.where.not(contributor_id:).update_all(linkage: "auxiliary") + end +end diff --git a/app/models/depositor_request.rb b/app/models/depositor_request.rb index 66344f19..2d145a6a 100644 --- a/app/models/depositor_request.rb +++ b/app/models/depositor_request.rb @@ -15,10 +15,13 @@ class DepositorRequest < ApplicationRecord scope :in_default_order, -> { order(created_at: :desc) } + delegate :auto_approve_depositors?, to: :submission_target delegate :entity, to: :submission_target, prefix: :target validates :user_id, uniqueness: { scope: :submission_target_id } + after_create :maybe_auto_approve! + # @see Access::Grant monadic_operation! def add_depositor call_operation("access.grant", Role.fetch(:depositor), on: target_entity, to: user).bind do @@ -26,6 +29,12 @@ class DepositorRequest < ApplicationRecord end end + # @api private + # @return [void] + def maybe_auto_approve! + transition_to!(:approved) if auto_approve_depositors? && pending? + end + # @see Access::Revoke monadic_operation! def remove_depositor call_operation("access.revoke", Role.fetch(:depositor), on: target_entity, to: user) diff --git a/app/models/schema_version.rb b/app/models/schema_version.rb index 3bdaafc3..a9d0a325 100644 --- a/app/models/schema_version.rb +++ b/app/models/schema_version.rb @@ -54,10 +54,12 @@ class SchemaVersion < ApplicationRecord with_options foreign_key: :source_id, inverse_of: :source, class_name: "SchemaVersionAssociation" do has_many_readonly :parent_associations, -> { by_name("parent") } has_many_readonly :child_associations, -> { by_name("child") } + has_many_readonly :submission_associations, -> { by_name("submission") } end has_many :enforced_parent_versions, through: :parent_associations, source: :target has_many :enforced_child_versions, through: :child_associations, source: :target + has_many :submittable_versions, through: :submission_associations, source: :target has_many :entity_links, dependent: :destroy diff --git a/app/models/submission.rb b/app/models/submission.rb index f92f9d4d..ea1dbca5 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -41,6 +41,26 @@ class Submission < ApplicationRecord validates :entity_id, uniqueness: { scope: :entity_type, if: :entity_id? } + # @!attribute [rw] agreement_accepted + # @return [Boolean] + def agreement_accepted = agreement_accepted_at? + + alias agreement_accepted? agreement_accepted + + def agreement_accepted=(value) + # :nocov: + self.agreement_accepted_at ||= Time.current if ActiveRecord::Type::Boolean.new.cast(value) + # :nocov: + end + + # @note Called during rejection. + # @see Submissions::Cleaner + # @see Submissions::CleanUp + # @return [Dry::Monads::Result] + monadic_operation! def clean_up + call_operation("submissions.clean_up", self) + end + # @see Submissions::ConstructDraftEntity # @see Submissions::DraftEntityConstructor # @return [Dry::Monads::Result] @@ -87,6 +107,14 @@ def owned_by(user) where(user:) end + def search_by_prefix(...) + where(entity_id: EntitySearchDocument.search_by_prefix(...).select(:entity_id)) + end + + def search_by_query(...) + where(entity_id: EntitySearchDocument.search_by_query(...).select(:entity_id)) + end + # @param [User, AnonymousUser] user # @return [ActiveRecord::Relation] def reviewable_by(user) diff --git a/app/models/user.rb b/app/models/user.rb index f1277003..7c08ce5a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -50,6 +50,14 @@ class User < ApplicationRecord has_many :submission_transitions, dependent: :nullify, inverse_of: :user + has_many :contributor_user_links, -> { in_default_order }, inverse_of: :user, dependent: :destroy + + has_many :contributors, through: :contributor_user_links + + has_one_readonly :primary_contributor_user_link, -> { primary_linkage }, class_name: "ContributorUserLink", inverse_of: :user + + has_one :primary_contributor, through: :primary_contributor_user_link, source: :contributor + validates :keycloak_id, presence: true attribute :global_access_control_list, Roles::GlobalAccessControlList.to_type diff --git a/app/operations/assets/decode_download_token.rb b/app/operations/assets/decode_download_token.rb index 8ba2815c..c37eee25 100644 --- a/app/operations/assets/decode_download_token.rb +++ b/app/operations/assets/decode_download_token.rb @@ -9,13 +9,15 @@ class DecodeDownloadToken # @param [Asset] asset # @param [String] token - # @return [Dry::Monads::Success(Boolean)] + # @return [Dry::Monads::Success("download" | "view")] def call(asset, token) return Failure[:missing_token] if token.blank? payload = yield decode.(token, aud: "download", sub: asset.id, verify_expiration: false) - Success payload.present? + mode = Assets::Types::AccessMode[payload["mode"]] + + Success mode end end end diff --git a/app/operations/assets/encode_download_token.rb b/app/operations/assets/encode_download_token.rb index f852d1b4..8b1ef13b 100644 --- a/app/operations/assets/encode_download_token.rb +++ b/app/operations/assets/encode_download_token.rb @@ -8,10 +8,12 @@ class EncodeDownloadToken ] # @param [Asset] asset + # @param ["download", "view"] mode # @return [Dry::Monads::Success(String)] - def call(asset, expires_at: 1.day.from_now) + def call(asset, mode: "download", expires_at: 2.weeks.from_now) payload = { aud: "download", sub: asset.id } + payload[:mode] = Assets::Types::AccessMode[mode] payload[:exp] = expires_at.to_i encode.(payload) diff --git a/app/operations/mutations/contracts/contributor_user_link_destroy.rb b/app/operations/mutations/contracts/contributor_user_link_destroy.rb new file mode 100644 index 00000000..18f31fba --- /dev/null +++ b/app/operations/mutations/contracts/contributor_user_link_destroy.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Mutations + module Contracts + # @see Mutations::ContributorUserLinkDestroy + # @see Mutations::Operations::ContributorUserLinkDestroy + class ContributorUserLinkDestroy < MutationOperations::Contract + json do + required(:contributor_user_link).value(:contributor_user_link) + end + end + end +end diff --git a/app/operations/mutations/contracts/contributor_user_link_upsert.rb b/app/operations/mutations/contracts/contributor_user_link_upsert.rb new file mode 100644 index 00000000..4d04d9b2 --- /dev/null +++ b/app/operations/mutations/contracts/contributor_user_link_upsert.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Mutations + module Contracts + # @see Mutations::ContributorUserLinkUpsert + # @see Mutations::Operations::ContributorUserLinkUpsert + class ContributorUserLinkUpsert < MutationOperations::Contract + json do + required(:contributor).value(:contributor) + required(:user).value(:user) + required(:linkage).value(:contributor_user_linkage) + end + end + end +end diff --git a/app/operations/mutations/contracts/submission_create.rb b/app/operations/mutations/contracts/submission_create.rb index 486d1663..b63e728b 100644 --- a/app/operations/mutations/contracts/submission_create.rb +++ b/app/operations/mutations/contracts/submission_create.rb @@ -10,6 +10,11 @@ class SubmissionCreate < MutationOperations::Contract required(:schema_version).value(:schema_version) required(:parent_entity).value(:any_entity) required(:title).filled(:string) + required(:agreement_accepted).value(:bool) + end + + rule(:agreement_accepted) do + base.failure(:depositor_agreement_required) unless value end end end diff --git a/app/operations/mutations/operations/contributor_user_link_destroy.rb b/app/operations/mutations/operations/contributor_user_link_destroy.rb new file mode 100644 index 00000000..bb684eb4 --- /dev/null +++ b/app/operations/mutations/operations/contributor_user_link_destroy.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Mutations + module Operations + # @see Mutations::ContributorUserLinkDestroy + class ContributorUserLinkDestroy + include MutationOperations::Base + + use_contract! :contributor_user_link_destroy + + authorizes! :contributor_user_link, with: :destroy? + + # @param [ContributorUserLink] contributor_user_link + # @return [void] + def call(contributor_user_link:) + destroy_model! contributor_user_link, auth: true + end + end + end +end diff --git a/app/operations/mutations/operations/contributor_user_link_upsert.rb b/app/operations/mutations/operations/contributor_user_link_upsert.rb new file mode 100644 index 00000000..a370c170 --- /dev/null +++ b/app/operations/mutations/operations/contributor_user_link_upsert.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Mutations + module Operations + # @see Mutations::ContributorUserLinkUpsert + class ContributorUserLinkUpsert + include MutationOperations::Base + + use_contract! :contributor_user_link_upsert + + authorizes! :contributor, with: :update? + + # @param [Contributor] contributor + # @param [User] user + # @param ["primary", "auxiliary"] linkage + # @return [void] + def call(contributor:, user:, linkage:, **) + link = ContributorUserLink.where(contributor:).first_or_initialize + + assign_attributes!(link, user:, linkage:) + + persist_model! link, attach_to: :contributor_user_link + + attach! :contributor, contributor.reload + attach! :user, user.reload + end + end + end +end diff --git a/app/operations/schemas/versions/maintain_associations.rb b/app/operations/schemas/versions/maintain_associations.rb index 6d17f481..8e77659c 100644 --- a/app/operations/schemas/versions/maintain_associations.rb +++ b/app/operations/schemas/versions/maintain_associations.rb @@ -98,6 +98,7 @@ def upsert_rows_for(source) mapping["parent"] = yield versions_for config.parents mapping["child"] = yield versions_for config.children + mapping["submission"] = yield versions_for config.submissions rows = mapping.flat_map do |(name, targets)| targets.map do |target| diff --git a/app/operations/submissions/clean_up.rb b/app/operations/submissions/clean_up.rb new file mode 100644 index 00000000..2079038f --- /dev/null +++ b/app/operations/submissions/clean_up.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Submissions + # @see Submissions::Cleaner + class CleanUp < Support::SimpleServiceOperation + service_klass Submissions::Cleaner + end +end diff --git a/app/policies/contributor_user_link_policy.rb b/app/policies/contributor_user_link_policy.rb new file mode 100644 index 00000000..67496c82 --- /dev/null +++ b/app/policies/contributor_user_link_policy.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# @see ContributorUserLink +class ContributorUserLinkPolicy < ApplicationPolicy + include PubliclyScopedPolicy + + pre_check :allow_any_admin! +end diff --git a/app/services/assets/types.rb b/app/services/assets/types.rb index c551c201..9853a76d 100644 --- a/app/services/assets/types.rb +++ b/app/services/assets/types.rb @@ -4,6 +4,8 @@ module Assets module Types extend ::Support::Typespace + AccessMode = Coercible::String.enum("download", "view").fallback("view") + Kind = ApplicationRecord.dry_pg_enum(:asset_kind, default: "unknown").fallback("unknown") end end diff --git a/app/services/filtering/scopes/items.rb b/app/services/filtering/scopes/items.rb new file mode 100644 index 00000000..aa5ecca4 --- /dev/null +++ b/app/services/filtering/scopes/items.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Filtering + module Scopes + class Items < Filtering::FilterScope[Item] + boolean_scope! :include_drafts, truthy_scope: :all, falsey_scope: :sans_drafts, default_value: false, replace_null: true do |arg| + arg.description <<~TEXT + Whether to include items that are in draft state (i.e. items that are associated with a submission). + TEXT + + timestamps! + end + end + end +end diff --git a/app/services/filtering/scopes/submissions.rb b/app/services/filtering/scopes/submissions.rb index 6965f615..04272b85 100644 --- a/app/services/filtering/scopes/submissions.rb +++ b/app/services/filtering/scopes/submissions.rb @@ -4,6 +4,10 @@ module Filtering module Scopes # The filtering scope implementation for {Submission}. class Submissions < ::Filtering::FilterScope[::Submission] + fts_search! :search_by_prefix, key: :prefix + + fts_search! :search_by_query, key: :query + simple_scope_filter! :parent_entity, :any_entities do |arg| arg.description <<~TEXT Filter submissions to only those with the given parent entity(ies). diff --git a/app/services/resolvers/item_resolver.rb b/app/services/resolvers/item_resolver.rb index 00e2744c..0c1f5a17 100644 --- a/app/services/resolvers/item_resolver.rb +++ b/app/services/resolvers/item_resolver.rb @@ -14,5 +14,7 @@ class ItemResolver < AbstractResolver type ::Types::ItemConnectionType, null: false resolves_model! ::Item, must_have_object: true + + filters_with! ::Filtering::Scopes::Items end end diff --git a/app/services/schemas/associations/submission.rb b/app/services/schemas/associations/submission.rb new file mode 100644 index 00000000..8b8fc775 --- /dev/null +++ b/app/services/schemas/associations/submission.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Schemas + module Associations + # An association representing an allowed submission type. + class Submission < Association + end + end +end diff --git a/app/services/schemas/properties/scalar/base.rb b/app/services/schemas/properties/scalar/base.rb index c5513f66..a85a0d0b 100644 --- a/app/services/schemas/properties/scalar/base.rb +++ b/app/services/schemas/properties/scalar/base.rb @@ -138,6 +138,10 @@ class Base < Schemas::Properties::BaseDefinition # @return [Schemas::Properties::Types::Function] attribute :function, :string, default: "unspecified" + # @!attribute [rw] instructions + # @return [String] + attribute :instructions, :string + # @!attribute [rw] mappings # @return [] attribute :mappings, Schemas::Properties::MappingDefinition.to_array_type @@ -146,6 +150,10 @@ class Base < Schemas::Properties::BaseDefinition # @return [Boolean] attribute :required, :boolean, default: proc { false } + # @!attribute [rw] submittable + # @return [Boolean] + attribute :submittable, :boolean, default: proc { false } + # @!attribute [rw] wide # @return [Boolean] attribute :wide, :boolean, default: proc { false } diff --git a/app/services/schemas/versions/configuration.rb b/app/services/schemas/versions/configuration.rb index 9b7c974a..006f37eb 100644 --- a/app/services/schemas/versions/configuration.rb +++ b/app/services/schemas/versions/configuration.rb @@ -71,6 +71,11 @@ class Configuration # @return [Schemas::Versions::RenderDefinition] attribute :render, Schemas::Versions::RenderDefinition.to_type, default: proc { {} } + # A collection of configurations for what kinds of {Submission submissions} can be made to an entity that implements this schema. + # + # @return [] + attribute :submissions, Schemas::Associations::Submission.to_array_type, default: proc { [] } + validates :kind, inclusion: { in: %w[community collection item] } validates :version, :namespace, :identifier, :name, presence: true diff --git a/app/services/shared/type_registry.rb b/app/services/shared/type_registry.rb index b38bcf8a..377b16e4 100644 --- a/app/services/shared/type_registry.rb +++ b/app/services/shared/type_registry.rb @@ -17,6 +17,7 @@ module Shared tc.add_model! "Collection" tc.add_model! "Community" tc.add_model! "Contributor" + tc.add_model! "ContributorUserLink" tc.add_model! "ControlledVocabulary" tc.add_model! "ControlledVocabularyItem" tc.add_model! "ControlledVocabularySource" @@ -42,6 +43,7 @@ module Shared tc.add_model! "SubmissionTargetReviewer" tc.add_enum! ::Types::ClientLocationType + tc.add_enum! ::Types::ContributorUserLinkageType tc.add_enum! ::Types::DepositorRequestStateType tc.add_enum! ::Types::EntitySubmissionStatusType tc.add_enum! ::Types::HarvestMetadataFormatType diff --git a/app/services/submissions/cleaner.rb b/app/services/submissions/cleaner.rb new file mode 100644 index 00000000..bbc8f511 --- /dev/null +++ b/app/services/submissions/cleaner.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Submissions + # @see Submission#clean_up + # @see Submissions::CleanUp + class Cleaner < Support::HookBased::Actor + include Dry::Initializer[undefined: false].define -> do + param :submission, Types::Submission + end + + standard_execution! + + # @return [Dry::Monads::Success(Submission)] + def call + run_callbacks :execute do + yield purge_entity! + end + + submission.reload + submission.reload_entity + + Success submission + end + + wrapped_hook! def purge_entity + # :nocov: + return Success() unless submission.entity + # :nocov: + + yield submission.entity.purge + + Success() + end + end +end diff --git a/app/services/submissions/publisher.rb b/app/services/submissions/publisher.rb index 37d254b4..327a3d0f 100644 --- a/app/services/submissions/publisher.rb +++ b/app/services/submissions/publisher.rb @@ -64,6 +64,8 @@ def actually_publish_entity! entity.submission_status = "submission_published" + entity.visibility = :visible + entity.save! end diff --git a/app/services/submissions/state_machine.rb b/app/services/submissions/state_machine.rb index 410ab803..ae9bfcfd 100644 --- a/app/services/submissions/state_machine.rb +++ b/app/services/submissions/state_machine.rb @@ -23,6 +23,8 @@ class StateMachine transition from: :under_review, to: :rejected transition from: :revision_requested, to: :submitted + transition from: :revision_requested, to: :approved + transition from: :revision_requested, to: :rejected transition from: :approved, to: :under_review transition from: :approved, to: :revision_requested @@ -31,5 +33,9 @@ class StateMachine after_transition do |submission, transition| submission.update_column(:state, transition.to_state) end + + after_transition to: :rejected do |submission, _transition| + submission.clean_up! + end end end diff --git a/app/services/templates/mdx/pdf_viewer_builder.rb b/app/services/templates/mdx/pdf_viewer_builder.rb index 5c7c52c5..8b4bae9b 100644 --- a/app/services/templates/mdx/pdf_viewer_builder.rb +++ b/app/services/templates/mdx/pdf_viewer_builder.rb @@ -11,7 +11,7 @@ class PDFViewerBuilder < ::Templates::MDX::AbstractBuilder option :asset, Templates::Types::Asset - option :url, Templates::Types::String, default: proc { asset.download_url } + option :url, Templates::Types::String, default: proc { asset.view_url } delegate :file_size, :original_filename, :system_slug, to: :asset diff --git a/config/initializers/900_good_job.rb b/config/initializers/900_good_job.rb index 5041ff10..f1ea4571 100644 --- a/config/initializers/900_good_job.rb +++ b/config/initializers/900_good_job.rb @@ -107,7 +107,7 @@ }, "harvesting.mappings.schedule_all_attempts": { cron: "30 6 * * *", - class: "Harvesting::Mappings::ScheduleAllAttempts", + class: "Harvesting::Mappings::ScheduleAllAttemptsJob", description: "Schedule all attempts once a day.", }, "ordering_invalidations.process_all": { diff --git a/config/locales/en.yml b/config/locales/en.yml index 5c6b60d6..330b4936 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -92,6 +92,7 @@ en: unacceptable_edge: "An entity of type '%{parent}' cannot be a direct parent of '%{child}'" update_and_clear_attachment: "cannot receive a new attachment while also clearing it" rules: + depositor_agreement_required: "You must accept the depositor agreement to submit." not_yet_implemented: "This mutation is not yet implemented." revalidation_connection_failed: "The revalidation request failed to connect." revalidation_request_failed: "The revalidation request failed." diff --git a/db/migrate/20260421173838_adjust_submissions.rb b/db/migrate/20260421173838_adjust_submissions.rb new file mode 100644 index 00000000..1b49d0b5 --- /dev/null +++ b/db/migrate/20260421173838_adjust_submissions.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AdjustSubmissions < ActiveRecord::Migration[8.1] + def change + change_table :submission_targets do |t| + t.boolean :auto_approve_depositors, null: false, default: false + end + + change_table :submissions do |t| + t.timestamp :agreement_accepted_at + end + + change_table :schema_version_properties do |t| + t.boolean :submittable, null: false, default: false + end + end +end diff --git a/db/migrate/20260422183557_create_contributor_user_links.rb b/db/migrate/20260422183557_create_contributor_user_links.rb new file mode 100644 index 00000000..9d13a465 --- /dev/null +++ b/db/migrate/20260422183557_create_contributor_user_links.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateContributorUserLinks < ActiveRecord::Migration[8.1] + def change + create_enum :contributor_user_linkage, %w[primary auxiliary] + + create_table :contributor_user_links, id: :uuid do |t| + t.references :contributor, null: false, foreign_key: { on_delete: :cascade }, type: :uuid, index: { unique: true } + t.references :user, null: false, foreign_key: { on_delete: :cascade }, type: :uuid + + t.enum :linkage, enum_type: "contributor_user_linkage", null: false, default: "auxiliary" + + t.timestamps null: false, default: -> { "CURRENT_TIMESTAMP" } + + t.index %i[user_id contributor_id linkage], unique: true, where: "linkage = 'primary'", name: "index_contributor_user_primary_link" + end + end +end diff --git a/db/structure.sql b/db/structure.sql index dc86600f..1991d06d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -255,6 +255,16 @@ CREATE TYPE public.contributor_list_filter AS ENUM ( ); +-- +-- Name: contributor_user_linkage; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.contributor_user_linkage AS ENUM ( + 'primary', + 'auxiliary' +); + + -- -- Name: date_precision; Type: TYPE; Schema: public; Owner: - -- @@ -4750,6 +4760,20 @@ CREATE MATERIALIZED VIEW public.contributor_attributions AS WITH NO DATA; +-- +-- Name: contributor_user_links; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.contributor_user_links ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + contributor_id uuid NOT NULL, + user_id uuid NOT NULL, + linkage public.contributor_user_linkage DEFAULT 'auxiliary'::public.contributor_user_linkage NOT NULL, + created_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + -- -- Name: contributors; Type: TABLE; Schema: public; Owner: - -- @@ -7040,7 +7064,8 @@ CREATE TABLE public.schema_version_properties ( updated_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, default_value jsonb GENERATED ALWAYS AS ((metadata -> 'default'::text)) STORED, function public.schema_property_function DEFAULT 'unspecified'::public.schema_property_function NOT NULL, - searchable boolean DEFAULT false NOT NULL + searchable boolean DEFAULT false NOT NULL, + submittable boolean DEFAULT false NOT NULL ); @@ -7420,7 +7445,8 @@ CREATE TABLE public.submission_targets ( agreement_content text, description jsonb DEFAULT '{}'::jsonb NOT NULL, created_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL + updated_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + auto_approve_depositors boolean DEFAULT false NOT NULL ); @@ -7462,7 +7488,8 @@ CREATE TABLE public.submissions ( title public.citext NOT NULL, metadata jsonb DEFAULT '{}'::jsonb NOT NULL, created_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL + updated_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + agreement_accepted_at timestamp without time zone ); @@ -8767,6 +8794,14 @@ ALTER TABLE ONLY public.contribution_role_configurations ADD CONSTRAINT contribution_role_configurations_pkey PRIMARY KEY (id); +-- +-- Name: contributor_user_links contributor_user_links_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.contributor_user_links + ADD CONSTRAINT contributor_user_links_pkey PRIMARY KEY (id); + + -- -- Name: contributors contributors_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -11246,6 +11281,27 @@ CREATE INDEX index_contributor_published_ranking ON public.contributor_attributi CREATE INDEX index_contributor_title_ranking ON public.contributor_attributions USING btree (contributor_id, title_rank); +-- +-- Name: index_contributor_user_links_on_contributor_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_contributor_user_links_on_contributor_id ON public.contributor_user_links USING btree (contributor_id); + + +-- +-- Name: index_contributor_user_links_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_contributor_user_links_on_user_id ON public.contributor_user_links USING btree (user_id); + + +-- +-- Name: index_contributor_user_primary_link; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_contributor_user_primary_link ON public.contributor_user_links USING btree (user_id, contributor_id, linkage) WHERE (linkage = 'primary'::public.contributor_user_linkage); + + -- -- Name: index_contributors_on_affiliation; Type: INDEX; Schema: public; Owner: - -- @@ -15511,6 +15567,14 @@ ALTER TABLE ONLY public.templates_list_item_instances ADD CONSTRAINT fk_rails_5a96c21277 FOREIGN KEY (template_definition_id) REFERENCES public.templates_list_item_definitions(id) ON DELETE CASCADE; +-- +-- Name: contributor_user_links fk_rails_5d62727433; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.contributor_user_links + ADD CONSTRAINT fk_rails_5d62727433 FOREIGN KEY (contributor_id) REFERENCES public.contributors(id) ON DELETE CASCADE; + + -- -- Name: item_attributions fk_rails_5d6b986800; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -16647,6 +16711,14 @@ ALTER TABLE ONLY public.ahoy_events ADD CONSTRAINT fk_rails_f1ed9fc4a0 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL; +-- +-- Name: contributor_user_links fk_rails_f229d9e76d; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.contributor_user_links + ADD CONSTRAINT fk_rails_f229d9e76d FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + -- -- Name: collection_contributions fk_rails_f2db32c240; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -16758,6 +16830,8 @@ ALTER TABLE ONLY public.templates_ordering_instances SET search_path TO "$user", public; INSERT INTO "schema_migrations" (version) VALUES +('20260422183557'), +('20260421173838'), ('20260316200244'), ('20260314094207'), ('20260312201100'), diff --git a/lib/schemas/definitions/nglp/book/1.0.0/config.json b/lib/schemas/definitions/nglp/book/1.0.0/config.json index 75b14154..564f7749 100644 --- a/lib/schemas/definitions/nglp/book/1.0.0/config.json +++ b/lib/schemas/definitions/nglp/book/1.0.0/config.json @@ -28,6 +28,12 @@ "identifier": "book_section" } ], + "submissions": [ + { + "namespace": "nglp", + "identifier": "book_section" + } + ], "orderings": [ { "id": "sections", diff --git a/lib/schemas/definitions/nglp/collected_article/1.0.0/config.json b/lib/schemas/definitions/nglp/collected_article/1.0.0/config.json index 25a7e583..dc8dba75 100644 --- a/lib/schemas/definitions/nglp/collected_article/1.0.0/config.json +++ b/lib/schemas/definitions/nglp/collected_article/1.0.0/config.json @@ -32,7 +32,8 @@ "type": "full_text", "required": false, "wide": true, - "function": "content" + "function": "content", + "submittable": true }, { "path": "abstract", @@ -40,7 +41,8 @@ "type": "full_text", "required": false, "wide": true, - "function": "content" + "function": "content", + "submittable": true }, { "path": "citation", @@ -67,7 +69,9 @@ "label": "PDF Version", "type": "asset", "required": false, - "function": "content" + "function": "content", + "submittable": true, + "instructions": "Upload a PDF version of the article. This will be displayed on the article page once published, as well as being available to download." }, { "path": "keywords", @@ -75,7 +79,8 @@ "type": "tags", "required": false, "function": "metadata", - "default": [] + "default": [], + "submittable": true }, { "path": "volume_position", diff --git a/lib/schemas/definitions/nglp/collected_volume/1.0.0/config.json b/lib/schemas/definitions/nglp/collected_volume/1.0.0/config.json index d4d871a3..cc1bbf31 100644 --- a/lib/schemas/definitions/nglp/collected_volume/1.0.0/config.json +++ b/lib/schemas/definitions/nglp/collected_volume/1.0.0/config.json @@ -24,6 +24,12 @@ "identifier": "collected_article" } ], + "submissions": [ + { + "namespace": "nglp", + "identifier": "collected_article" + } + ], "orderings": [ { "id": "articles", diff --git a/lib/schemas/definitions/nglp/dissertation/1.0.0/config.json b/lib/schemas/definitions/nglp/dissertation/1.0.0/config.json index 8f73d235..a5ea4289 100644 --- a/lib/schemas/definitions/nglp/dissertation/1.0.0/config.json +++ b/lib/schemas/definitions/nglp/dissertation/1.0.0/config.json @@ -6,18 +6,86 @@ "kind": "item", "orderings": [], "properties": [ - { "path": "abstract", "label": "Abstract", "type": "full_text", "required": false, "wide": true, "function": "content" }, - { "path": "citation", "label": "How to Cite", "type": "markdown", "required": false, "function": "content" }, - { "path": "pdf_version", "label": "PDF Version", "type": "asset", "required": false, "function": "content" }, - { "path": "text_version", "label": "Text Version", "type": "asset", "required": false, "function": "content" }, - { "path": "publisher", "type": "string", "required": false, "function": "metadata" }, - { "path": "advisor", "type": "string", "required": false, "function": "metadata" }, - { "path": "language", "label": "language", "type": "string", "required": false, "function": "metadata" }, - { "path": "graduation_date", "label": "Graduation Date", "type": "variable_date", "function": "metadata" }, - { "path": "streaming_media", "label": "Streaming Media", "type": "url", "function": "metadata" }, - { "path": "keywords", "label": "Keywords", "type": "tags", "required": false, "function": "metadata", "default": [] }, - { "path": "language_code", "type": "string", "label": "Language Code" }, - { "path": "handle", "label": "Handle", "type": "url", "required": false, "function": "metadata" }, + { + "path": "abstract", + "label": "Abstract", + "type": "full_text", + "required": false, + "wide": true, + "function": "content" + }, + { + "path": "citation", + "label": "How to Cite", + "type": "markdown", + "required": false, + "function": "content" + }, + { + "path": "pdf_version", + "label": "PDF Version", + "type": "asset", + "required": false, + "function": "content" + }, + { + "path": "text_version", + "label": "Text Version", + "type": "asset", + "required": false, + "function": "content" + }, + { + "path": "publisher", + "type": "string", + "required": false, + "function": "metadata" + }, + { + "path": "advisor", + "type": "string", + "required": false, + "function": "metadata" + }, + { + "path": "language", + "label": "language", + "type": "string", + "required": false, + "function": "metadata" + }, + { + "path": "graduation_date", + "label": "Graduation Date", + "type": "variable_date", + "function": "metadata" + }, + { + "path": "streaming_media", + "label": "Streaming Media", + "type": "url", + "function": "metadata" + }, + { + "path": "keywords", + "label": "Keywords", + "type": "tags", + "required": false, + "function": "metadata", + "default": [] + }, + { + "path": "language_code", + "type": "string", + "label": "Language Code" + }, + { + "path": "handle", + "label": "Handle", + "type": "url", + "required": false, + "function": "metadata" + }, { "type": "select", "path": "cc_license", @@ -49,31 +117,73 @@ } ] }, - { "path": "rights_statement", "label": "Rights Statement", "type": "full_text", "required": false, "wide": true, "function": "metadata" }, - { "path": "peer_reviewed", "label": "Peer Reviewed", "type": "boolean", "function": "metadata" }, + { + "path": "rights_statement", + "label": "Rights Statement", + "type": "full_text", + "required": false, + "wide": true, + "function": "metadata" + }, + { + "path": "peer_reviewed", + "label": "Peer Reviewed", + "type": "boolean", + "function": "metadata" + }, { "path": "degree", "type": "group", "legend": "Degree Details", "properties": [ { - "label": "Degree Name", "path": "name", "type": "string", "function": "metadata" + "label": "Degree Name", + "path": "name", + "type": "string", + "function": "metadata" }, { - "label": "Degree Level", "path": "level", "type": "string", "required": false, "function": "metadata" + "label": "Degree Level", + "path": "level", + "type": "string", + "required": false, + "function": "metadata" }, { - "label": "Degree Department", "path": "program", "type": "string", "function": "metadata" + "label": "Degree Department", + "path": "program", + "type": "string", + "function": "metadata" }, { - "label": "Degree Grantor", "path": "grantor", "type": "string", "required": false, "function": "metadata" + "label": "Degree Grantor", + "path": "grantor", + "type": "string", + "required": false, + "function": "metadata" }, { - "label": "Degree Awarded Date", "path": "date", "type": "variable_date", "required": false, "function": "metadata" + "label": "Degree Awarded Date", + "path": "date", + "type": "variable_date", + "required": false, + "function": "metadata" } ] }, - { "path": "accessioned", "type": "variable_date", "label": "Accessioned", "description": "When the ETD was added to a collection", "function": "metadata" }, - { "path": "available", "type": "variable_date", "label": "Available", "description": "When the ETD was made available", "function": "metadata" } + { + "path": "accessioned", + "type": "variable_date", + "label": "Accessioned", + "description": "When the ETD was added to a collection", + "function": "metadata" + }, + { + "path": "available", + "type": "variable_date", + "label": "Available", + "description": "When the ETD was made available", + "function": "metadata" + } ] } diff --git a/lib/schemas/definitions/nglp/journal/1.0.0/config.json b/lib/schemas/definitions/nglp/journal/1.0.0/config.json index 7d7617ca..d37d0c1a 100644 --- a/lib/schemas/definitions/nglp/journal/1.0.0/config.json +++ b/lib/schemas/definitions/nglp/journal/1.0.0/config.json @@ -18,6 +18,16 @@ "identifier": "collected_volume" } ], + "submissions": [ + { + "namespace": "nglp", + "identifier": "journal_article" + }, + { + "namespace": "nglp", + "identifier": "collected_article" + } + ], "orderings": [ { "id": "articles", diff --git a/lib/schemas/definitions/nglp/journal_article/1.0.0/config.json b/lib/schemas/definitions/nglp/journal_article/1.0.0/config.json index c44016ef..1f4c6c60 100644 --- a/lib/schemas/definitions/nglp/journal_article/1.0.0/config.json +++ b/lib/schemas/definitions/nglp/journal_article/1.0.0/config.json @@ -32,20 +32,86 @@ ], "orderings": [], "properties": [ - { "path": "body", "label": "Full Text", "type": "full_text", "required": false, "wide": true, "function": "content" }, - { "path": "abstract", "label": "Abstract", "type": "full_text", "required": false, "wide": true, "function": "content" }, - { "path": "citation", "label": "How to Cite", "type": "markdown", "required": false, "function": "content" }, - { "path": "preprint_version", "type": "boolean", "label": "Pre-Print Version", "function": "metadata" }, - { "path": "online_version", "label": "Online Version", "type": "url", "required": false, "function": "content" }, - { "path": "pdf_version", "label": "PDF Version", "type": "asset", "required": false, "function": "content" }, - { "path": "keywords", "label": "Keywords", "type": "tags", "required": false, "function": "metadata", "default": [] }, - { "path": "issue_position", "label": "Issue Position", "type": "integer", "function": "metadata", "description": "The position of the article within the issue." }, + { + "path": "body", + "label": "Full Text", + "type": "full_text", + "required": false, + "wide": true, + "function": "content", + "submittable": true + }, + { + "path": "abstract", + "label": "Abstract", + "type": "full_text", + "required": false, + "wide": true, + "function": "content", + "submittable": true + }, + { + "path": "citation", + "label": "How to Cite", + "type": "markdown", + "required": false, + "function": "content" + }, + { + "path": "preprint_version", + "type": "boolean", + "label": "Pre-Print Version", + "function": "metadata" + }, + { + "path": "online_version", + "label": "Online Version", + "type": "url", + "required": false, + "function": "content" + }, + { + "path": "pdf_version", + "label": "PDF Version", + "type": "asset", + "required": false, + "function": "content", + "submittable": true, + "instructions": "Upload a PDF version of the article. This will be displayed on the article page once published, as well as being available to download." + }, + { + "path": "keywords", + "label": "Keywords", + "type": "tags", + "required": false, + "function": "metadata", + "default": [], + "submittable": true + }, + { + "path": "issue_position", + "label": "Issue Position", + "type": "integer", + "function": "metadata", + "description": "The position of the article within the issue." + }, { "path": "meta", "legend": "Article Metadata", "properties": [ - { "path": "collected", "label": "Collected", "type": "variable_date", "function": "metadata" }, - { "path": "page_count", "label": "Page Count", "type": "integer", "function": "metadata", "unorderable": true } + { + "path": "collected", + "label": "Collected", + "type": "variable_date", + "function": "metadata" + }, + { + "path": "page_count", + "label": "Page Count", + "type": "integer", + "function": "metadata", + "unorderable": true + } ], "type": "group" } diff --git a/lib/schemas/definitions/nglp/journal_issue/1.0.0/config.json b/lib/schemas/definitions/nglp/journal_issue/1.0.0/config.json index 18b910e6..d85cb698 100644 --- a/lib/schemas/definitions/nglp/journal_issue/1.0.0/config.json +++ b/lib/schemas/definitions/nglp/journal_issue/1.0.0/config.json @@ -34,6 +34,12 @@ "identifier": "journal_article" } ], + "submissions": [ + { + "namespace": "nglp", + "identifier": "journal_article" + } + ], "orderings": [ { "id": "articles", diff --git a/lib/schemas/definitions/nglp/journal_volume/1.0.0/config.json b/lib/schemas/definitions/nglp/journal_volume/1.0.0/config.json index 4e0cac48..e31911e1 100644 --- a/lib/schemas/definitions/nglp/journal_volume/1.0.0/config.json +++ b/lib/schemas/definitions/nglp/journal_volume/1.0.0/config.json @@ -24,6 +24,12 @@ "identifier": "journal_issue" } ], + "submissions": [ + { + "namespace": "nglp", + "identifier": "journal_article" + } + ], "orderings": [ { "id": "articles", diff --git a/lib/schemas/definitions/nglp/paper/1.0.0/config.json b/lib/schemas/definitions/nglp/paper/1.0.0/config.json index 4ea0a1e5..047727ee 100644 --- a/lib/schemas/definitions/nglp/paper/1.0.0/config.json +++ b/lib/schemas/definitions/nglp/paper/1.0.0/config.json @@ -6,16 +6,79 @@ "kind": "item", "orderings": [], "properties": [ - { "path": "abstract", "label": "Abstract", "type": "full_text", "required": false, "wide": true, "function": "content" }, - { "path": "funding_note", "type": "string", "label": "Funding Note", "required": false }, - { "path": "rights_statement", "type": "string", "label": "Rights Statement" }, - { "path": "pdf_version", "label": "PDF Version", "type": "asset", "required": false, "function": "content" }, - { "path": "text_version", "label": "Text Version", "type": "asset", "required": false, "function": "content" }, - { "path": "accessioned", "type": "variable_date", "label": "Accessioned", "description": "When the paper was added to a collection", "function": "metadata" }, - { "path": "available", "type": "variable_date", "label": "Available", "description": "When the paper was made available", "function": "metadata" }, - { "path": "language_code", "type": "string", "label": "Language Code" }, - { "path": "keywords", "label": "Keywords", "type": "tags", "required": false, "function": "metadata", "default": [] }, - { "path": "handle", "label": "Handle", "type": "url", "required": false, "function": "metadata" }, + { + "path": "abstract", + "label": "Abstract", + "type": "full_text", + "required": false, + "wide": true, + "function": "content", + "submittable": true + }, + { + "path": "funding_note", + "type": "string", + "label": "Funding Note", + "required": false + }, + { + "path": "rights_statement", + "type": "string", + "label": "Rights Statement" + }, + { + "path": "pdf_version", + "label": "PDF Version", + "type": "asset", + "required": false, + "function": "content", + "submittable": true, + "instructions": "Upload a PDF version of the paper. This will be visible on the web once published, as well as being made available to download." + }, + { + "path": "text_version", + "label": "Text Version", + "type": "asset", + "required": false, + "function": "content", + "submittable": true, + "instructions": "Upload a text version of the paper. This will be made available to download once published." + }, + { + "path": "accessioned", + "type": "variable_date", + "label": "Accessioned", + "description": "When the paper was added to a collection", + "function": "metadata" + }, + { + "path": "available", + "type": "variable_date", + "label": "Available", + "description": "When the paper was made available", + "function": "metadata" + }, + { + "path": "language_code", + "type": "string", + "label": "Language Code" + }, + { + "path": "keywords", + "label": "Keywords", + "type": "tags", + "required": false, + "function": "metadata", + "default": [], + "submittable": true + }, + { + "path": "handle", + "label": "Handle", + "type": "url", + "required": false, + "function": "metadata" + }, { "type": "group", "path": "publisher", @@ -23,8 +86,18 @@ "description": "Details about the publisher of the paper", "function": "presentation", "properties": [ - { "path": "name", "type": "string", "required": false, "function": "presentation" }, - { "path": "location", "type": "string", "required": false, "function": "presentation" } + { + "path": "name", + "type": "string", + "required": false, + "function": "presentation" + }, + { + "path": "location", + "type": "string", + "required": false, + "function": "presentation" + } ] }, { @@ -34,12 +107,50 @@ "description": "Details about the source of the paper", "function": "presentation", "properties": [ - { "path": "title", "type": "string", "required": false, "function": "presentation" }, - { "path": "volume", "type": "string", "required": false, "function": "presentation", "unorderable": true }, - { "path": "issue", "type": "string", "required": false, "function": "presentation", "unorderable": true }, - { "path": "fpage", "type": "integer", "label": "First Page", "required": false, "function": "metadata", "unorderable": true }, - { "path": "lpage", "type": "integer", "label": "Last Page", "required": false, "function": "metadata", "unorderable": true }, - { "path": "page_count", "type": "integer", "label": "Page Count", "required": false, "function": "metadata", "unorderable": true } + { + "path": "title", + "type": "string", + "required": false, + "function": "presentation" + }, + { + "path": "volume", + "type": "string", + "required": false, + "function": "presentation", + "unorderable": true + }, + { + "path": "issue", + "type": "string", + "required": false, + "function": "presentation", + "unorderable": true + }, + { + "path": "fpage", + "type": "integer", + "label": "First Page", + "required": false, + "function": "metadata", + "unorderable": true + }, + { + "path": "lpage", + "type": "integer", + "label": "Last Page", + "required": false, + "function": "metadata", + "unorderable": true + }, + { + "path": "page_count", + "type": "integer", + "label": "Page Count", + "required": false, + "function": "metadata", + "unorderable": true + } ] } ] diff --git a/lib/schemas/definitions/nglp/series/1.0.0/config.json b/lib/schemas/definitions/nglp/series/1.0.0/config.json index e4948c8b..2155ec39 100644 --- a/lib/schemas/definitions/nglp/series/1.0.0/config.json +++ b/lib/schemas/definitions/nglp/series/1.0.0/config.json @@ -18,6 +18,16 @@ "identifier": "series" } ], + "submissions": [ + { + "namespace": "nglp", + "identifier": "paper" + }, + { + "namespace": "nglp", + "identifier": "dissertation" + } + ], "orderings": [ { "id": "etd", diff --git a/lib/schemas/metaschemas/entity-definition/1.0.0.json b/lib/schemas/metaschemas/entity-definition/1.0.0.json index 97a7cb76..450c80d4 100644 --- a/lib/schemas/metaschemas/entity-definition/1.0.0.json +++ b/lib/schemas/metaschemas/entity-definition/1.0.0.json @@ -670,6 +670,11 @@ "type": "string" }, "description": { "$ref": "#/definitions/PropertyDescription" }, + "instructions": { + "title": "Depositor Instructions", + "description": "Markdown content that can be used to provide instructions for filling out the field in a form.", + "type": "string" + }, "mappings": { "$ref": "#/definitions/PropertyMappings" }, "required": { "title": "Required?", @@ -677,6 +682,12 @@ "type": "boolean", "default": false }, + "submittable": { + "title": "Submittable?", + "description": "Whether the property should be included in submission forms.", + "type": "boolean", + "default": false + }, "unorderable": { "title": "Unorderable?", "description": "This can be toggled on if the property _could_ be ordered, but shouldn't be. Strings that will not sort logically, numbers that are not intended for sorting, etc." , @@ -1184,6 +1195,10 @@ "$ref": "#/definitions/Associations", "description": "The type(s) of direct children this schema accepts." }, + "submissions": { + "$ref": "#/definitions/Associations", + "description": "The type(s) of submissions this schema accepts." + }, "orderings": { "title": "Orderings", "description": "A schema can provide default orderings, which an instance can override.", diff --git a/spec/factories/contributor_user_links.rb b/spec/factories/contributor_user_links.rb new file mode 100644 index 00000000..0e9aa9f2 --- /dev/null +++ b/spec/factories/contributor_user_links.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :contributor_user_link do + association :contributor, :person + association :user + + linkage { "auxiliary" } + + trait :primary do + linkage { "primary" } + end + + trait :auxiliary do + linkage { "auxiliary" } + end + end +end diff --git a/spec/factories/submissions.rb b/spec/factories/submissions.rb index 619a6a12..edb5899f 100644 --- a/spec/factories/submissions.rb +++ b/spec/factories/submissions.rb @@ -9,6 +9,10 @@ title { Faker::Lorem.sentence } + trait :item do + association :schema_version, :item + end + trait :submitted do after(:create) do |submission| submission.transition_to! :submitted diff --git a/spec/models/contributor_user_link_spec.rb b/spec/models/contributor_user_link_spec.rb new file mode 100644 index 00000000..97112f8f --- /dev/null +++ b/spec/models/contributor_user_link_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe ContributorUserLink, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/operations/assets/decode_download_token_spec.rb b/spec/operations/assets/decode_download_token_spec.rb index 13426bd2..e33e6b10 100644 --- a/spec/operations/assets/decode_download_token_spec.rb +++ b/spec/operations/assets/decode_download_token_spec.rb @@ -3,9 +3,47 @@ RSpec.describe Assets::DecodeDownloadToken, type: :operation do let_it_be(:asset, refind: true) { FactoryBot.create :asset } - let!(:token) { MeruAPI::Container["assets.encode_download_token"].(asset, expires_at: 2.weeks.ago).value! } + let(:expected_mode) { "download" } + + let(:mode) { "download" } + let(:expires_at) { 2.weeks.ago } + + let(:token_options) do + { + expires_at:, + mode:, + } + end + + let!(:token) { asset.encode_download_token!(**token_options) } it "decodes regardless of the expiration date" do - expect_calling_with(asset, token).to succeed.with(true) + expect_calling_with(asset, token).to succeed.with(expected_mode) + end + + context "with a view token" do + let(:mode) { "view" } + let(:expected_mode) { "view" } + + it "decodes the mode correctly" do + expect_calling_with(asset, token).to succeed.with(expected_mode) + end + end + + context "with an unknown mode" do + let(:mode) { "unknown_mode" } + let(:expected_mode) { "view" } + + it "falls back to view mode" do + expect_calling_with(asset, token).to succeed.with(expected_mode) + end + end + + context "with a missing token" do + let(:token) { nil } + + it "returns a failure" do + expect_calling_with(asset, token).to monad_fail.with_key(:missing_token) + end end end diff --git a/spec/operations/submission_targets/batch_publish_spec.rb b/spec/operations/submission_targets/batch_publish_spec.rb index eac0371f..29fb330e 100644 --- a/spec/operations/submission_targets/batch_publish_spec.rb +++ b/spec/operations/submission_targets/batch_publish_spec.rb @@ -36,8 +36,6 @@ ) end - let_it_be(:rejected_entity, refind: true) { rejected_submission.entity } - let_it_be(:user, refind: true) { FactoryBot.create(:user) } let(:submissions) { [approved_submission, rejected_submission] } @@ -59,7 +57,6 @@ expect do flush_enqueued_jobs end.to change { approved_entity.reload.submission_status }.from("submission_draft").to("submission_published") - .and keep_the_same { rejected_entity.reload.submission_status } .and change { approved_submission.current_state(force_reload: true) }.from("approved").to("published") .and keep_the_same { rejected_submission.current_state(force_reload: true) } .and change(SubmissionBatchPublicationTransition.to_finished, :count).by(1) diff --git a/spec/policies/item_policy_spec.rb b/spec/policies/item_policy_spec.rb index 8a3ec295..f6f204e5 100644 --- a/spec/policies/item_policy_spec.rb +++ b/spec/policies/item_policy_spec.rb @@ -9,6 +9,12 @@ let_it_be(:other_item, refind: true) { FactoryBot.create :item, title: "Other Item" } + let_it_be(:submission, refind: true) { FactoryBot.create :submission, :item } + + let_it_be(:submission_item, refind: true) do + submission.entity + end + let_it_be(:contextual_role) { FactoryBot.create :role, :all_contextual } let(:record) { item } @@ -157,19 +163,24 @@ let(:user) { admin } it "includes everything" do - is_expected.to include item, subitem, other_item + is_expected.to include item, subitem, other_item, submission_item end end context "as a user with all contextual permissions" do before do grant_access! contextual_role, on: item, to: user + other_item.update!(visibility: :hidden) end it "excludes hidden records" do is_expected.to exclude(other_item).and include(item, subitem) end + + it "excludes unpublished records" do + is_expected.to exclude(submission_item) + end end context "as a random user" do @@ -178,6 +189,10 @@ it "excludes hidden records" do is_expected.to exclude(other_item).and include(item, subitem) end + + it "excludes unpublished records" do + is_expected.to exclude(submission_item) + end end context "as an anonymous user" do @@ -188,6 +203,10 @@ it "excludes hidden records" do is_expected.to exclude(other_item).and include(item, subitem) end + + it "excludes unpublished records" do + is_expected.to exclude(submission_item) + end end end end diff --git a/spec/requests/downloads_spec.rb b/spec/requests/downloads_spec.rb index 0718c586..2e664027 100644 --- a/spec/requests/downloads_spec.rb +++ b/spec/requests/downloads_spec.rb @@ -13,11 +13,28 @@ def make_request! get download_path(id, token:) end - context "with a valid token" do - let(:token) { existing_asset.download_token } + context "with a valid view token" do + let(:token) { existing_asset.encode_download_token!(mode: "view") } + + it "redirects to a PDF viewer url" do + expect do + safely_make_request! + end.to change(Ahoy::Event.where(name: "asset.view"), :count).by(1) + .and keep_the_same(Ahoy::Event.where(name: "asset.download"), :count) + + expect(response).to redirect_to existing_asset.actual_download_url + expect(response).to have_http_status :see_other + end + end + + context "with a valid download token" do + let(:token) { existing_asset.encode_download_token!(mode: "download") } it "redirects to a download url" do - safely_make_request! + expect do + safely_make_request! + end.to change(Ahoy::Event.where(name: "asset.download"), :count).by(1) + .and keep_the_same(Ahoy::Event.where(name: "asset.view"), :count) expect(response).to redirect_to existing_asset.actual_download_url expect(response).to have_http_status :see_other diff --git a/spec/requests/graphql/mutations/contributor_user_link_destroy_spec.rb b/spec/requests/graphql/mutations/contributor_user_link_destroy_spec.rb new file mode 100644 index 00000000..5e61f20a --- /dev/null +++ b/spec/requests/graphql/mutations/contributor_user_link_destroy_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +RSpec.describe Mutations::ContributorUserLinkDestroy, type: :request, graphql: :mutation do + mutation_query! <<~GRAPHQL + mutation ContributorUserLinkDestroy($input: ContributorUserLinkDestroyInput!) { + contributorUserLinkDestroy(input: $input) { + destroyed + destroyedId + ... ErrorFragment + } + } + GRAPHQL + + let_it_be(:existing_contributor_user_link_attrs) do + {} + end + + let_it_be(:existing_contributor_user_link) { FactoryBot.create(:contributor_user_link, **existing_contributor_user_link_attrs) } + + let_mutation_input!(:contributor_user_link_id) { existing_contributor_user_link.to_encoded_id } + + let(:valid_mutation_shape) do + gql.mutation(:contributor_user_link_destroy) do |m| + m[:destroyed] = true + m[:destroyed_id] = be_an_encoded_id.of_a_deleted_model + end + end + + let(:empty_mutation_shape) do + gql.empty_mutation :contributor_user_link_destroy + end + + shared_examples_for "a successful mutation" do + let(:expected_shape) { valid_mutation_shape } + + it "destroys the contributor user link" do + expect_request! do |req| + req.effect! change(ContributorUserLink, :count).by(-1) + + req.data! expected_shape + end + end + end + + shared_examples_for "an unauthorized mutation" do + let(:expected_shape) { empty_mutation_shape } + + it "is not authorized" do + expect_request! do |req| + req.effect! execute_safely + req.effect! keep_the_same(ContributorUserLink, :count) + + req.unauthorized! + + req.data! expected_shape + end + end + end + + shared_examples_for "an authorized mutation" do + include_examples "a successful mutation" + end + + as_an_admin_user do + include_examples "an authorized mutation" + end + + as_a_regular_user do + include_examples "an unauthorized mutation" + end + + as_an_anonymous_user do + include_examples "an unauthorized mutation" + end +end diff --git a/spec/requests/graphql/mutations/contributor_user_link_upsert_spec.rb b/spec/requests/graphql/mutations/contributor_user_link_upsert_spec.rb new file mode 100644 index 00000000..b8d74e1e --- /dev/null +++ b/spec/requests/graphql/mutations/contributor_user_link_upsert_spec.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +RSpec.describe Mutations::ContributorUserLinkUpsert, type: :request, graphql: :mutation do + mutation_query! <<~GRAPHQL + mutation ContributorUserLinkUpsert($input: ContributorUserLinkUpsertInput!) { + contributorUserLinkUpsert(input: $input) { + contributor { + ... ContributorFragment + } + + user { + ... UserFragment + } + + contributorUserLink { + id + slug + linkage + + contributor { + ... ContributorFragment + } + + user { + ... UserFragment + } + } + + ... ErrorFragment + } + } + + fragment ContributorFragment on Contributor { + ... on Node { + id + } + + ... on Sluggable { + slug + } + + ... on ContributorBase { + kind + } + } + + fragment UserFragment on User { + id + slug + + primaryContributor { + ... ContributorFragment + } + } + GRAPHQL + + let_it_be(:other_contributor, refind: true) do + FactoryBot.create(:contributor, :person) + end + let_it_be(:other_user, refind: true) { FactoryBot.create(:user) } + + let_it_be(:contributor, refind: true) { FactoryBot.create(:contributor, :person) } + + let_it_be(:user, refind: true) { FactoryBot.create(:user) } + + let_mutation_input!(:contributor_id) { contributor.to_encoded_id } + let_mutation_input!(:user_id) { user.to_encoded_id } + let_mutation_input!(:linkage) { "PRIMARY" } + + let(:valid_mutation_shape) do + gql.mutation(:contributor_user_link_upsert) do |m| + m.prop(:contributor) do |c| + c[:id] = be_an_encoded_id.of_an_existing_model + c[:slug] = be_an_encoded_slug + end + end + end + + let(:empty_mutation_shape) do + gql.empty_mutation :contributor_user_link_upsert + end + + shared_examples_for "a successful mutation" do + let(:expected_shape) { valid_mutation_shape } + + it "links the contributor and the user" do + expect_request! do |req| + req.effect! change(ContributorUserLink, :count).by(1) + + req.effect! change { user.reload_primary_contributor }.from(nil).to(contributor) + + req.data! expected_shape + end + end + end + + shared_examples_for "an unauthorized mutation" do + let(:expected_shape) { empty_mutation_shape } + + it "is not authorized" do + expect_request! do |req| + req.effect! execute_safely + + req.unauthorized! + + req.data! expected_shape + end + end + end + + shared_examples_for "an authorized mutation" do + include_examples "a successful mutation" + + context "when the contributor / user link pair already exists" do + let!(:existing_link) do + FactoryBot.create(:contributor_user_link, contributor:, user:, linkage: "auxiliary") + end + + it "updates the existing link" do + expect_request! do |req| + req.effect! change { existing_link.reload.linkage }.from("auxiliary").to("primary") + end + end + end + + context "when a primary link already exists for the user" do + let!(:existing_link) do + FactoryBot.create(:contributor_user_link, :primary, user:, contributor: other_contributor) + end + + it "overrides the existing primary link" do + expect_request! do |req| + req.effect! change { existing_link.reload.linkage }.from("primary").to("auxiliary") + end + end + end + + context "when a link already exists for the contributor but with a different user" do + let!(:existing_link) do + FactoryBot.create(:contributor_user_link, contributor:, user: other_user, linkage: "primary") + end + + it "updates the existing link to point to the new user" do + expect_request! do |req| + req.effect! change { other_user.reload_primary_contributor }.from(contributor).to(nil) + req.effect! change { existing_link.reload.user }.from(existing_link.user).to(user) + end + end + end + end + + as_an_admin_user do + include_examples "an authorized mutation" + end + + as_a_regular_user do + include_examples "an unauthorized mutation" + end + + as_an_anonymous_user do + include_examples "an unauthorized mutation" + end +end diff --git a/spec/requests/graphql/mutations/submission_create_spec.rb b/spec/requests/graphql/mutations/submission_create_spec.rb index 35c3ef82..e45b858e 100644 --- a/spec/requests/graphql/mutations/submission_create_spec.rb +++ b/spec/requests/graphql/mutations/submission_create_spec.rb @@ -8,6 +8,8 @@ id slug + agreementAcceptedAt + submissionTarget { id @@ -49,6 +51,7 @@ let_mutation_input!(:schema_version_id) { item_schema_version.to_encoded_id } let_mutation_input!(:parent_entity_id) { collection.to_encoded_id } let_mutation_input!(:title) { "Test Submission" } + let_mutation_input!(:agreement_accepted) { true } let(:valid_mutation_shape) do gql.mutation(:submission_create) do |m| @@ -56,6 +59,8 @@ s[:id] = be_an_encoded_id.of_an_existing_model s[:slug] = be_an_encoded_slug + s[:agreement_accepted_at] = be_present + s.prop :submission_target do |st| st[:id] = submission_target_id @@ -110,6 +115,27 @@ shared_examples_for "an authorized mutation" do include_examples "a successful mutation" + + context "when the agreement is not accepted" do + let_mutation_input!(:agreement_accepted) { false } + + let(:expected_shape) do + gql.mutation(:submission_create) do |m| + m[:submission] = nil + m.global_errors do |ge| + ge.error :depositor_agreement_required + end + end + end + + it "fails validation" do + expect_request! do |req| + req.effect! keep_the_same(Submission, :count) + + req.data! expected_shape + end + end + end end as_an_admin_user do diff --git a/spec/requests/graphql/mutations/submission_publish_spec.rb b/spec/requests/graphql/mutations/submission_publish_spec.rb index 93ecb2a9..b40087f5 100644 --- a/spec/requests/graphql/mutations/submission_publish_spec.rb +++ b/spec/requests/graphql/mutations/submission_publish_spec.rb @@ -81,8 +81,6 @@ ) end - let_it_be(:rejected_entity, refind: true) { rejected_submission.entity } - let_mutation_input!(:submission_id) { approved_submission.to_encoded_id } let(:valid_mutation_shape) do @@ -140,7 +138,6 @@ req.effect! keep_the_same(SubmissionPublication, :count) req.effect! keep_the_same(SubmissionPublicationTransition, :count) req.effect! keep_the_same { rejected_submission.current_state(force_reload: true) } - req.effect! keep_the_same { rejected_entity.reload.submission_status } req.data! expected_shape end