diff --git a/Gemfile b/Gemfile index 66c7f739..ccfa6f8a 100644 --- a/Gemfile +++ b/Gemfile @@ -149,13 +149,15 @@ 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 "faker", "~> 3.6" gem "rspec", "~> 3.13.2" gem "rspec-rails", "~> 8.0" - gem "yard", "~> 0.9.34" - gem "yard-activerecord", "~> 0.0.16" - gem "yard-activesupport-concern", "~> 0.0.1" + 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 end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 77f92b65..ad6d2463 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -117,7 +117,7 @@ GEM anyway_config (2.8.0) ruby-next-core (~> 1.0) ast (2.4.3) - async (2.36.0) + async (2.38.0) console (~> 1.29) fiber-annotation io-event (~> 1.11) @@ -126,7 +126,7 @@ GEM attr_required (1.0.2) autotuner (1.1.0) aws-eventstream (1.4.0) - aws-partitions (1.1222.0) + aws-partitions (1.1223.0) aws-sdk-core (3.243.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -147,6 +147,7 @@ GEM base64 (0.3.0) bcrypt (3.1.21) benchmark (0.5.0) + benchmark-ips (2.14.0) bibtex-ruby (6.2.0) latex-decode (~> 0.0) logger (~> 1.7) @@ -182,6 +183,24 @@ GEM database_cleaner-core (~> 2.0.0) redis date (3.5.1) + derailed_benchmarks (2.2.1) + base64 + benchmark-ips (~> 2) + bigdecimal + drb + get_process_mem + heapy (~> 0) + logger + memory_profiler (>= 0, < 2) + mini_histogram (>= 0.3.0) + mutex_m + ostruct + rack (>= 1) + rack-test + rake (> 10, < 14) + ruby-statistics (>= 4.0.1) + ruby2_keywords + thor (>= 0.19, < 2) device_detector (1.1.3) diff-lcs (1.6.2) docile (1.4.1) @@ -310,6 +329,9 @@ GEM geocoder (1.8.6) base64 (>= 0.1.0) csv (>= 3.0.0) + get_process_mem (1.0.0) + bigdecimal (>= 2.0) + ffi (~> 1.0) globalid (1.3.0) activesupport (>= 6.1) good_job (4.13.3) @@ -343,7 +365,7 @@ GEM google-protobuf (4.34.0-x86_64-linux-musl) bigdecimal rake (~> 13.3) - graphql (2.5.20) + graphql (2.5.21) base64 fiber-storage logger @@ -360,6 +382,8 @@ GEM hana (1.3.7) hashdiff (1.2.1) hashids (1.0.6) + heapy (0.2.0) + thor htmlbeautifier (1.4.3) http (5.3.1) addressable (~> 2.8) @@ -377,7 +401,7 @@ GEM mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) io-console (0.8.2) - io-event (1.14.3) + io-event (1.14.4) irb (1.17.0) pp (>= 0.6.0) prism (>= 1.3.0) @@ -391,7 +415,7 @@ GEM jmespath (1.6.2) job-iteration (1.12.0) activejob (>= 6.1) - json (2.18.1) + json (2.19.1) json-jwt (1.17.0) activesupport (>= 4.2) aes_key_wrap @@ -457,12 +481,14 @@ GEM marcel (1.1.0) maxminddb (0.1.22) mediainfo (1.5.0) + memory_profiler (1.1.0) method_source (1.1.0) metrics (0.15.0) mime-types (3.7.0) logger mime-types-data (~> 3.2025, >= 3.2025.0507) mime-types-data (3.2026.0303) + mini_histogram (0.3.1) mini_magick (5.3.1) logger mini_mime (1.1.5) @@ -481,6 +507,7 @@ GEM multi_json (1.19.1) mustermann (3.0.4) ruby2_keywords (~> 0.0.1) + mutex_m (0.3.0) namae (1.2.0) racc (~> 1.7) naught (1.1.0) @@ -652,7 +679,7 @@ GEM redcarpet (3.6.1) redis (5.4.1) redis-client (>= 0.22.0) - redis-client (0.26.4) + redis-client (0.27.0) connection_pool redis-objects (2.0.0) redis (~> 5.0) @@ -685,14 +712,14 @@ GEM rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (8.0.3) + rspec-rails (8.0.4) actionpack (>= 7.2) activesupport (>= 7.2) railties (>= 7.2) - rspec-core (~> 3.13) - rspec-expectations (~> 3.13) - rspec-mocks (~> 3.13) - rspec-support (~> 3.13) + rspec-core (>= 3.13.0, < 5.0.0) + rspec-expectations (>= 3.13.0, < 5.0.0) + rspec-mocks (>= 3.13.0, < 5.0.0) + rspec-support (>= 3.13.0, < 5.0.0) rspec-support (3.13.7) rubocop (1.79.2) json (~> 2.3) @@ -730,6 +757,7 @@ GEM base64 ostruct ruby-progressbar (1.13.0) + ruby-statistics (4.1.0) ruby-vips (2.3.0) ffi (~> 1.12) logger @@ -802,7 +830,7 @@ GEM thor (1.5.0) tilt (2.7.0) timecop (0.9.10) - timeout (0.6.0) + timeout (0.6.1) tomlib (0.7.3) bigdecimal traces (0.18.2) @@ -820,6 +848,7 @@ GEM validate_url (1.0.15) activemodel (>= 3.0.0) public_suffix + vernier (1.10.0) wapiti (2.1.0) builder (~> 3.2) rexml (~> 3.0) @@ -886,6 +915,7 @@ DEPENDENCIES csv (~> 3.3.5) database_cleaner-active_record (~> 2.2.2) database_cleaner-redis (~> 2.0.0) + derailed_benchmarks down (~> 5.4.2) dry-auto_inject (~> 1.1) dry-core (~> 1.2) @@ -998,6 +1028,7 @@ DEPENDENCIES tomlib (~> 0.7.2) tus-server (~> 2.3.0) validate_url (~> 1.0.15) + vernier webmock (= 3.26.1) with_advisory_lock (~> 7.0.2) yard (~> 0.9.34) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 3830dd5e..a914434a 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -32,6 +32,7 @@ def execute auth_context:, current_user:, request_state:, + visibility_profile: :public, } json = request_state.wrap do diff --git a/app/graphql/api_schema.rb b/app/graphql/api_schema.rb index 7390c8e4..367f8250 100644 --- a/app/graphql/api_schema.rb +++ b/app/graphql/api_schema.rb @@ -14,6 +14,8 @@ class APISchema < GraphQL::Schema trace_with(GraphQL::Tracing::ActiveSupportNotificationsTrace) trace_with(GraphQL::Tracing::NewRelicTrace) + introspection ::Support::GraphQLAPI::Introspection + validate_timeout nil use GraphQL::FragmentCache @@ -94,6 +96,7 @@ class APISchema < GraphQL::Schema Types::OrganizationContributorType, Types::PersonContributorType, Types::SearchResultType, + Types::SchemaPropertyFunctionType, Types::SchemaPropertyTypeType, Types::SchemaInstanceType, Types::TemplateContributionType @@ -180,4 +183,10 @@ def resolve_type(abstract_type, object, context) end end end + + # @note This has to be at the end of the file so that extra and orphan types + # are properly registered. + use GraphQL::Schema::Visibility, dynamic: false, preload: true, profiles: { + public: { public: true }, + } end diff --git a/app/graphql/mutations/depositor_agreement_accept.rb b/app/graphql/mutations/depositor_agreement_accept.rb new file mode 100644 index 00000000..0e9d8985 --- /dev/null +++ b/app/graphql/mutations/depositor_agreement_accept.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Mutations + # @see Mutations::Operations::DepositorAgreementAccept + class DepositorAgreementAccept < Mutations::BaseMutation + description <<~TEXT + Accept the depositor agreement for the given submission target. + TEXT + + field :depositor_agreement, Types::DepositorAgreementType, null: true do + description "The depositor agreement that was accepted." + end + + argument :submission_target_id, ID, required: true, loads: Types::SubmissionTargetType do + description "The ID of the submission target that the agreement belongs to." + end + + performs_operation! "mutations.operations.depositor_agreement_accept" + end +end diff --git a/app/graphql/mutations/depositor_agreement_reset.rb b/app/graphql/mutations/depositor_agreement_reset.rb new file mode 100644 index 00000000..ebc9e0b3 --- /dev/null +++ b/app/graphql/mutations/depositor_agreement_reset.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Mutations + # @see Mutations::Operations::DepositorAgreementReset + class DepositorAgreementReset < Mutations::BaseMutation + description <<~TEXT + Reset a specific depositor agreement, forcing the associated depositor to re-accept the agreement before making any more deposits to the associated submission target. + TEXT + + field :depositor_agreement, Types::DepositorAgreementType, null: true do + description "The depositor agreement that was reset." + end + + argument :submission_target_id, ID, required: true, loads: Types::SubmissionTargetType do + description "The ID of the submission target for which to reset a depositor agreement." + end + + argument :user_id, ID, required: true, loads: Types::UserType do + description "The ID of the user for which to reset a depositor agreement." + end + + performs_operation! "mutations.operations.depositor_agreement_reset" + end +end diff --git a/app/graphql/mutations/depositor_agreement_reset_all.rb b/app/graphql/mutations/depositor_agreement_reset_all.rb new file mode 100644 index 00000000..18662aa3 --- /dev/null +++ b/app/graphql/mutations/depositor_agreement_reset_all.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Mutations + # @see Mutations::Operations::DepositorAgreementResetAll + class DepositorAgreementResetAll < Mutations::BaseMutation + description <<~TEXT + Force all depositors to re-accept the depositor agreement for a given submission target. + TEXT + + field :submission_target, Types::SubmissionTargetType, null: true do + description "The submission target for which depositor agreements were reset." + end + + argument :submission_target_id, ID, required: true, loads: Types::SubmissionTargetType do + description "The ID of the submission target for which to reset depositor agreements." + end + + performs_operation! "mutations.operations.depositor_agreement_reset_all" + end +end diff --git a/app/graphql/mutations/submission_batch_publish.rb b/app/graphql/mutations/submission_batch_publish.rb new file mode 100644 index 00000000..8f8f23c9 --- /dev/null +++ b/app/graphql/mutations/submission_batch_publish.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Mutations + # @see Mutations::Operations::SubmissionBatchPublish + class SubmissionBatchPublish < Mutations::BaseMutation + description <<~TEXT + Publish multiple submissions within a single submission target. + + This will enqueue the actual publications in the backend. + TEXT + + field :submission_batch_publication, Types::SubmissionBatchPublicationType, null: true do + description "The submission batch publication that was created to track this process." + end + + field :submission_target, Types::SubmissionTargetType, null: true do + description "The submission target that the submissions belong to." + end + + argument :submission_target_id, ID, required: true, loads: Types::SubmissionTargetType do + description "The ID of the submission target that the submissions belong to." + end + + argument :submission_ids, [ID], required: true, loads: Types::SubmissionType do + description "The IDs of the submissions to publish." + end + + performs_operation! "mutations.operations.submission_batch_publish" + end +end diff --git a/app/graphql/mutations/submission_publish.rb b/app/graphql/mutations/submission_publish.rb new file mode 100644 index 00000000..bb533473 --- /dev/null +++ b/app/graphql/mutations/submission_publish.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Mutations + # @see Mutations::Operations::SubmissionPublish + class SubmissionPublish < Mutations::BaseMutation + description <<~TEXT + Publish a single submission. + + To publish multiple submissions at once, use `submissionBatchPublish`. + TEXT + + field :entity, Types::EntityType, null: true do + description <<~TEXT + The entity that the published submission belongs to, if successful. + TEXT + end + + field :submission, Types::SubmissionType, null: true do + description <<~TEXT + The submission that was published, if successful. + TEXT + end + + field :submission_publication, Types::SubmissionPublicationType, null: true do + description <<~TEXT + The actual record of the publication, if successful. + TEXT + end + + argument :submission_id, ID, required: true, loads: ::Types::SubmissionType do + description <<~TEXT + The ID for the submission to publish. + TEXT + end + + performs_operation! "mutations.operations.submission_publish" + end +end diff --git a/app/graphql/types/asset_kind_type.rb b/app/graphql/types/asset_kind_type.rb index 39a5ec39..0a0caa58 100644 --- a/app/graphql/types/asset_kind_type.rb +++ b/app/graphql/types/asset_kind_type.rb @@ -9,6 +9,7 @@ class AssetKindType < Types::BaseEnum value "audio" value "pdf" value "document" + value "archive" value "unknown" end end diff --git a/app/graphql/types/depositor_agreement_connection_type.rb b/app/graphql/types/depositor_agreement_connection_type.rb new file mode 100644 index 00000000..78266965 --- /dev/null +++ b/app/graphql/types/depositor_agreement_connection_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + # A connection type for {DepositorAgreement}-typed records. + # + # @see DepositorAgreement + # @see ::Types::DepositorAgreementEdgeType + # @see ::Types::DepositorAgreementType + class DepositorAgreementConnectionType < Types::BaseConnection + graphql_name "DepositorAgreementConnection" + + description <<~TEXT + A connection type for `DepositorAgreement`. + TEXT + + edge_type ::Types::DepositorAgreementEdgeType + end +end diff --git a/app/graphql/types/depositor_agreement_edge_type.rb b/app/graphql/types/depositor_agreement_edge_type.rb new file mode 100644 index 00000000..60842aa6 --- /dev/null +++ b/app/graphql/types/depositor_agreement_edge_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + # An edge in a {::Types::DepositorAgreementConnectionType} for {DepositorAgreement}-type records. + # + # @see DepositorAgreement + # @see ::Types::DepositorAgreementConnectionType + # @see ::Types::DepositorAgreementType + class DepositorAgreementEdgeType < Types::BaseEdge + description <<~TEXT + An edge in a connection for `DepositorAgreement`. + TEXT + + node_type ::Types::DepositorAgreementType + end +end diff --git a/app/graphql/types/depositor_agreement_state_type.rb b/app/graphql/types/depositor_agreement_state_type.rb new file mode 100644 index 00000000..043602a3 --- /dev/null +++ b/app/graphql/types/depositor_agreement_state_type.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + class DepositorAgreementStateType < Types::BaseEnum + description <<~TEXT + The state of a user's acceptance of a given `SubmissionTarget`'s agreement requirements. + TEXT + + value "PENDING", value: "pending" do + description <<~TEXT + The user has not yet accepted the agreement for the submission target. + TEXT + end + + value "ACCEPTED", value: "accepted" do + description <<~TEXT + The user has accepted the agreement for the submission target. + TEXT + end + end +end diff --git a/app/graphql/types/depositor_agreement_transition_connection_type.rb b/app/graphql/types/depositor_agreement_transition_connection_type.rb new file mode 100644 index 00000000..478c1b6e --- /dev/null +++ b/app/graphql/types/depositor_agreement_transition_connection_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + # A connection type for {DepositorAgreementTransition}-typed records. + # + # @see DepositorAgreementTransition + # @see ::Types::DepositorAgreementTransitionEdgeType + # @see ::Types::DepositorAgreementTransitionType + class DepositorAgreementTransitionConnectionType < Types::BaseConnection + graphql_name "DepositorAgreementTransitionConnection" + + description <<~TEXT + A connection type for `DepositorAgreementTransition`. + TEXT + + edge_type ::Types::DepositorAgreementTransitionEdgeType + end +end diff --git a/app/graphql/types/depositor_agreement_transition_edge_type.rb b/app/graphql/types/depositor_agreement_transition_edge_type.rb new file mode 100644 index 00000000..e600f083 --- /dev/null +++ b/app/graphql/types/depositor_agreement_transition_edge_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + # An edge in a {::Types::DepositorAgreementTransitionConnectionType} for {DepositorAgreementTransition}-type records. + # + # @see DepositorAgreementTransition + # @see ::Types::DepositorAgreementTransitionConnectionType + # @see ::Types::DepositorAgreementTransitionType + class DepositorAgreementTransitionEdgeType < Types::BaseEdge + description <<~TEXT + An edge in a connection for `DepositorAgreementTransition`. + TEXT + + node_type ::Types::DepositorAgreementTransitionType + end +end diff --git a/app/graphql/types/depositor_agreement_transition_type.rb b/app/graphql/types/depositor_agreement_transition_type.rb new file mode 100644 index 00000000..03fdba32 --- /dev/null +++ b/app/graphql/types/depositor_agreement_transition_type.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Types + # @see DepositorAgreementTransition + # @see ::Types::DepositorAgreementTransitionConnectionType + # @see ::Types::DepositorAgreementTransitionEdgeType + class DepositorAgreementTransitionType < Types::AbstractModel + description <<~TEXT + A transition for a `DepositorAgreement`. + TEXT + + use_direct_connection_and_edge! + + implements ::Types::CommonTransitionType + + field :from_state, Types::DepositorAgreementStateType, null: true do + description <<~TEXT + The state that the depositor agreement is transitioning from. This will be null if the submission target is being created. + TEXT + end + + field :to_state, Types::DepositorAgreementStateType, null: false do + description <<~TEXT + The state that the depositor agreement is transitioning to. + TEXT + end + end +end diff --git a/app/graphql/types/depositor_agreement_type.rb b/app/graphql/types/depositor_agreement_type.rb new file mode 100644 index 00000000..f4ad6aa0 --- /dev/null +++ b/app/graphql/types/depositor_agreement_type.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Types + # @see DepositorAgreement + # @see ::Types::DepositorAgreementConnectionType + # @see ::Types::DepositorAgreementEdgeType + class DepositorAgreementType < Types::AbstractModel + description <<~TEXT + The record of an agreement accepted by a depositor for a given submission target. + TEXT + + use_direct_connection_and_edge! + + field :state, Types::DepositorAgreementStateType, null: false do + description <<~TEXT + The current state of the agreement. + TEXT + end + + field :submission_target, "Types::SubmissionTargetType", null: false do + description <<~TEXT + The submission target associated with this agreement. + TEXT + end + + field :user, "Types::UserType", null: true do + description <<~TEXT + The user who accepted the agreement. + TEXT + end + + field :last_accepted_at, GraphQL::Types::ISO8601DateTime, null: true do + description <<~TEXT + The timestamp of the most recent time the agreement was accepted by the depositor. + TEXT + end + + field :transitions, resolver: Resolvers::DepositorAgreementTransitionResolver, null: false do + description <<~TEXT + The state transitions for this agreement. + TEXT + end + + expose_authorization_rule :accept?, <<~TEXT + Whether the current user can accept this agreement. + TEXT + + expose_authorization_rule :reset?, <<~TEXT + Whether the current user can reset this agreement. + TEXT + + load_association! :submission_target + + load_association! :user + end +end diff --git a/app/graphql/types/depositor_request_type.rb b/app/graphql/types/depositor_request_type.rb index ee39ab71..e0a2fd0c 100644 --- a/app/graphql/types/depositor_request_type.rb +++ b/app/graphql/types/depositor_request_type.rb @@ -35,6 +35,12 @@ class DepositorRequestType < Types::AbstractModel TEXT end + field :transitions, resolver: Resolvers::DepositorRequestTransitionResolver, null: false do + description <<~TEXT + The state transitions for this depositor request. + TEXT + end + expose_authorization_rule :transition?, <<~TEXT Whether the current user can approve or reject the request. TEXT diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 7bf2f4b5..8e72165a 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -36,6 +36,16 @@ class MutationType < Types::BaseObject field :create_role, mutation: Mutations::CreateRole + field :depositor_agreement_accept, mutation: Mutations::DepositorAgreementAccept + + field :depositor_agreement_reset, mutation: Mutations::DepositorAgreementReset + + field :depositor_agreement_reset_all, mutation: Mutations::DepositorAgreementResetAll + + field :depositor_request_change_state, mutation: Mutations::DepositorRequestChangeState + + field :depositor_request_create, mutation: Mutations::DepositorRequestCreate + field :destroy_announcement, mutation: Mutations::DestroyAnnouncement field :destroy_asset, mutation: Mutations::DestroyAsset @@ -64,10 +74,10 @@ class MutationType < Types::BaseObject field :grant_access, mutation: Mutations::GrantAccess - field :harvest_attempt_from_source, mutation: Mutations::HarvestAttemptFromSource - field :harvest_attempt_from_mapping, mutation: Mutations::HarvestAttemptFromMapping + field :harvest_attempt_from_source, mutation: Mutations::HarvestAttemptFromSource + field :harvest_attempt_prune_entities, mutation: Mutations::HarvestAttemptPruneEntities field :harvest_mapping_create, mutation: Mutations::HarvestMappingCreate @@ -90,6 +100,12 @@ class MutationType < Types::BaseObject field :link_entity, mutation: Mutations::LinkEntity + field :permalink_create, mutation: Mutations::PermalinkCreate + + field :permalink_destroy, mutation: Mutations::PermalinkDestroy + + field :permalink_update, mutation: Mutations::PermalinkUpdate + field :preview_slot, mutation: Mutations::PreviewSlot field :render_layouts, mutation: Mutations::RenderLayouts @@ -100,8 +116,34 @@ class MutationType < Types::BaseObject field :revoke_access, mutation: Mutations::RevokeAccess + field :submission_batch_publish, mutation: Mutations::SubmissionBatchPublish + + field :submission_change_state, mutation: Mutations::SubmissionChangeState + + field :submission_comment_create, mutation: Mutations::SubmissionCommentCreate + + field :submission_comment_destroy, mutation: Mutations::SubmissionCommentDestroy + + field :submission_comment_update, mutation: Mutations::SubmissionCommentUpdate + + field :submission_create, mutation: Mutations::SubmissionCreate + + field :submission_leave_review, mutation: Mutations::SubmissionLeaveReview + + field :submission_publish, mutation: Mutations::SubmissionPublish + + field :submission_request_review, mutation: Mutations::SubmissionRequestReview + + field :submission_target_close, mutation: Mutations::SubmissionTargetClose + field :submission_target_configure, mutation: Mutations::SubmissionTargetConfigure + field :submission_target_open, mutation: Mutations::SubmissionTargetOpen + + field :submission_target_reviewer_create, mutation: Mutations::SubmissionTargetReviewerCreate + + field :submission_target_reviewer_destroy, mutation: Mutations::SubmissionTargetReviewerDestroy + field :update_announcement, mutation: Mutations::UpdateAnnouncement field :update_asset, mutation: Mutations::UpdateAsset @@ -114,10 +156,10 @@ class MutationType < Types::BaseObject field :update_contribution, mutation: Mutations::UpdateContribution - field :update_item, mutation: Mutations::UpdateItem - field :update_global_configuration, mutation: Mutations::UpdateGlobalConfiguration + field :update_item, mutation: Mutations::UpdateItem + field :update_ordering, mutation: Mutations::UpdateOrdering field :update_organization_contributor, mutation: Mutations::UpdateOrganizationContributor @@ -135,37 +177,5 @@ class MutationType < Types::BaseObject field :upsert_contribution, mutation: Mutations::UpsertContribution field :user_reset_password, mutation: Mutations::UserResetPassword - - field :permalink_create, mutation: Mutations::PermalinkCreate - - field :permalink_destroy, mutation: Mutations::PermalinkDestroy - - field :permalink_update, mutation: Mutations::PermalinkUpdate - - field :submission_target_reviewer_create, mutation: Mutations::SubmissionTargetReviewerCreate - - field :submission_target_reviewer_destroy, mutation: Mutations::SubmissionTargetReviewerDestroy - - field :submission_change_state, mutation: Mutations::SubmissionChangeState - - field :submission_create, mutation: Mutations::SubmissionCreate - - field :submission_target_open, mutation: Mutations::SubmissionTargetOpen - - field :submission_target_close, mutation: Mutations::SubmissionTargetClose - - field :submission_comment_create, mutation: Mutations::SubmissionCommentCreate - - field :submission_comment_destroy, mutation: Mutations::SubmissionCommentDestroy - - field :submission_comment_update, mutation: Mutations::SubmissionCommentUpdate - - field :depositor_request_create, mutation: Mutations::DepositorRequestCreate - - field :depositor_request_change_state, mutation: Mutations::DepositorRequestChangeState - - field :submission_request_review, mutation: Mutations::SubmissionRequestReview - - field :submission_leave_review, mutation: Mutations::SubmissionLeaveReview end end diff --git a/app/graphql/types/submission_batch_publication_connection_type.rb b/app/graphql/types/submission_batch_publication_connection_type.rb new file mode 100644 index 00000000..ed8a031e --- /dev/null +++ b/app/graphql/types/submission_batch_publication_connection_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + # A connection type for {SubmissionBatchPublication}-typed records. + # + # @see SubmissionBatchPublication + # @see ::Types::SubmissionBatchPublicationEdgeType + # @see ::Types::SubmissionBatchPublicationType + class SubmissionBatchPublicationConnectionType < Types::BaseConnection + graphql_name "SubmissionBatchPublicationConnection" + + description <<~TEXT + A connection type for `SubmissionBatchPublication`. + TEXT + + edge_type ::Types::SubmissionBatchPublicationEdgeType + end +end diff --git a/app/graphql/types/submission_batch_publication_edge_type.rb b/app/graphql/types/submission_batch_publication_edge_type.rb new file mode 100644 index 00000000..b79cdfca --- /dev/null +++ b/app/graphql/types/submission_batch_publication_edge_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + # An edge in a {::Types::SubmissionBatchPublicationConnectionType} for {SubmissionBatchPublication}-type records. + # + # @see SubmissionBatchPublication + # @see ::Types::SubmissionBatchPublicationConnectionType + # @see ::Types::SubmissionBatchPublicationType + class SubmissionBatchPublicationEdgeType < Types::BaseEdge + description <<~TEXT + An edge in a connection for `SubmissionBatchPublication`. + TEXT + + node_type ::Types::SubmissionBatchPublicationType + end +end diff --git a/app/graphql/types/submission_batch_publication_state_type.rb b/app/graphql/types/submission_batch_publication_state_type.rb new file mode 100644 index 00000000..168997b6 --- /dev/null +++ b/app/graphql/types/submission_batch_publication_state_type.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Types + # @see SubmissionBatchPublication + # @see SubmissionBatchPublicationTransition + # @see SubmissionBatchPublications::StateMachine + class SubmissionBatchPublicationStateType < Types::BaseEnum + description <<~TEXT + The state of a batch of submission publications. + TEXT + + value "PENDING", value: "pending" do + description <<~TEXT + The batch of submissions is pending publication. + TEXT + end + + value "BATCHED", value: "batched" do + description <<~TEXT + The batch of submissions has been batched for publication and will be processed in the background. + TEXT + end + + value "FINISHED", value: "finished" do + description <<~TEXT + The batch of submissions has finished. + TEXT + end + end +end diff --git a/app/graphql/types/submission_batch_publication_transition_connection_type.rb b/app/graphql/types/submission_batch_publication_transition_connection_type.rb new file mode 100644 index 00000000..7bf03ad4 --- /dev/null +++ b/app/graphql/types/submission_batch_publication_transition_connection_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + # A connection type for {SubmissionBatchPublicationTransition}-typed records. + # + # @see SubmissionBatchPublicationTransition + # @see ::Types::SubmissionBatchPublicationTransitionEdgeType + # @see ::Types::SubmissionBatchPublicationTransitionType + class SubmissionBatchPublicationTransitionConnectionType < Types::BaseConnection + graphql_name "SubmissionBatchPublicationTransitionConnection" + + description <<~TEXT + A connection type for `SubmissionBatchPublicationTransition`. + TEXT + + edge_type ::Types::SubmissionBatchPublicationTransitionEdgeType + end +end diff --git a/app/graphql/types/submission_batch_publication_transition_edge_type.rb b/app/graphql/types/submission_batch_publication_transition_edge_type.rb new file mode 100644 index 00000000..97816097 --- /dev/null +++ b/app/graphql/types/submission_batch_publication_transition_edge_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + # An edge in a {::Types::SubmissionBatchPublicationTransitionConnectionType} for {SubmissionBatchPublicationTransition}-type records. + # + # @see SubmissionBatchPublicationTransition + # @see ::Types::SubmissionBatchPublicationTransitionConnectionType + # @see ::Types::SubmissionBatchPublicationTransitionType + class SubmissionBatchPublicationTransitionEdgeType < Types::BaseEdge + description <<~TEXT + An edge in a connection for `SubmissionBatchPublicationTransition`. + TEXT + + node_type ::Types::SubmissionBatchPublicationTransitionType + end +end diff --git a/app/graphql/types/submission_batch_publication_transition_type.rb b/app/graphql/types/submission_batch_publication_transition_type.rb new file mode 100644 index 00000000..059950c9 --- /dev/null +++ b/app/graphql/types/submission_batch_publication_transition_type.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Types + # @see SubmissionBatchPublicationTransition + # @see ::Types::SubmissionBatchPublicationTransitionConnectionType + # @see ::Types::SubmissionBatchPublicationTransitionEdgeType + class SubmissionBatchPublicationTransitionType < Types::AbstractModel + description <<~TEXT + A transition for a `SubmissionBatchPublication`. + TEXT + + use_direct_connection_and_edge! + + implements ::Types::CommonTransitionType + + field :from_state, Types::SubmissionBatchPublicationStateType, null: true do + description <<~TEXT + The state that the submission batch publication is transitioning from. This will be null if the submission target is being created. + TEXT + end + + field :to_state, Types::SubmissionBatchPublicationStateType, null: false do + description <<~TEXT + The state that the submission batch publication is transitioning to. + TEXT + end + end +end diff --git a/app/graphql/types/submission_batch_publication_type.rb b/app/graphql/types/submission_batch_publication_type.rb new file mode 100644 index 00000000..6117592a --- /dev/null +++ b/app/graphql/types/submission_batch_publication_type.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Types + # @see SubmissionBatchPublication + # @see ::Types::SubmissionBatchPublicationConnectionType + # @see ::Types::SubmissionBatchPublicationEdgeType + class SubmissionBatchPublicationType < Types::AbstractModel + description <<~TEXT + The record of a batch publication of one or more submissions within a submission target. + TEXT + + use_direct_connection_and_edge! + + field :state, Types::SubmissionBatchPublicationStateType, null: false do + description <<~TEXT + The current state of the batch publication process. + TEXT + end + + field :submission_target, Types::SubmissionTargetType, null: false do + description <<~TEXT + The submission target that the included submissions belong to. + TEXT + end + + field :user, Types::UserType, null: true do + description <<~TEXT + The user that initiated the batch publication, if applicable. + TEXT + end + + field :publications, [Types::SubmissionPublicationType, { null: false }], null: false do + description <<~TEXT + The list of submissions included in this batch publication. + TEXT + end + + field :publications_count, Integer, null: false do + description <<~TEXT + The total number of submissions included in this batch publication. + TEXT + end + + field :transitions, resolver: Resolvers::SubmissionBatchPublicationTransitionResolver, null: false do + description <<~TEXT + The transitions that the batch publication process has gone through. + TEXT + end + + load_association! :submission_target + + load_association! :user + + load_association! :submission_publications, as: :publications + end +end diff --git a/app/graphql/types/submission_publication_connection_type.rb b/app/graphql/types/submission_publication_connection_type.rb new file mode 100644 index 00000000..92a2d42f --- /dev/null +++ b/app/graphql/types/submission_publication_connection_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + # A connection type for {SubmissionPublication}-typed records. + # + # @see SubmissionPublication + # @see ::Types::SubmissionPublicationEdgeType + # @see ::Types::SubmissionPublicationType + class SubmissionPublicationConnectionType < Types::BaseConnection + graphql_name "SubmissionPublicationConnection" + + description <<~TEXT + A connection type for `SubmissionPublication`. + TEXT + + edge_type ::Types::SubmissionPublicationEdgeType + end +end diff --git a/app/graphql/types/submission_publication_edge_type.rb b/app/graphql/types/submission_publication_edge_type.rb new file mode 100644 index 00000000..c6791e85 --- /dev/null +++ b/app/graphql/types/submission_publication_edge_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + # An edge in a {::Types::SubmissionPublicationConnectionType} for {SubmissionPublication}-type records. + # + # @see SubmissionPublication + # @see ::Types::SubmissionPublicationConnectionType + # @see ::Types::SubmissionPublicationType + class SubmissionPublicationEdgeType < Types::BaseEdge + description <<~TEXT + An edge in a connection for `SubmissionPublication`. + TEXT + + node_type ::Types::SubmissionPublicationType + end +end diff --git a/app/graphql/types/submission_publication_state_type.rb b/app/graphql/types/submission_publication_state_type.rb new file mode 100644 index 00000000..e8b40636 --- /dev/null +++ b/app/graphql/types/submission_publication_state_type.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Types + # @see SubmissionPublication + # @see SubmissionPublicationTransition + # @see SubmissionPublications::StateMachine + class SubmissionPublicationStateType < Types::BaseEnum + description <<~TEXT + The state of a submission's publication. + TEXT + + value "PENDING", value: "pending" do + description <<~TEXT + The submission is pending publication. + TEXT + end + + value "BATCHED", value: "batched" do + description <<~TEXT + The submission has been batched for publication and will be processed in the background. + TEXT + end + + value "SUCCESS", value: "success" do + description <<~TEXT + The submission has been successfully published. + TEXT + end + + value "FAILURE", value: "failure" do + description <<~TEXT + The submission failed to publish. + TEXT + end + end +end diff --git a/app/graphql/types/submission_publication_transition_connection_type.rb b/app/graphql/types/submission_publication_transition_connection_type.rb new file mode 100644 index 00000000..c8901e9a --- /dev/null +++ b/app/graphql/types/submission_publication_transition_connection_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + # A connection type for {SubmissionPublicationTransition}-typed records. + # + # @see SubmissionPublicationTransition + # @see ::Types::SubmissionPublicationTransitionEdgeType + # @see ::Types::SubmissionPublicationTransitionType + class SubmissionPublicationTransitionConnectionType < Types::BaseConnection + graphql_name "SubmissionPublicationTransitionConnection" + + description <<~TEXT + A connection type for `SubmissionPublicationTransition`. + TEXT + + edge_type ::Types::SubmissionPublicationTransitionEdgeType + end +end diff --git a/app/graphql/types/submission_publication_transition_edge_type.rb b/app/graphql/types/submission_publication_transition_edge_type.rb new file mode 100644 index 00000000..639ed150 --- /dev/null +++ b/app/graphql/types/submission_publication_transition_edge_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + # An edge in a {::Types::SubmissionPublicationTransitionConnectionType} for {SubmissionPublicationTransition}-type records. + # + # @see SubmissionPublicationTransition + # @see ::Types::SubmissionPublicationTransitionConnectionType + # @see ::Types::SubmissionPublicationTransitionType + class SubmissionPublicationTransitionEdgeType < Types::BaseEdge + description <<~TEXT + An edge in a connection for `SubmissionPublicationTransition`. + TEXT + + node_type ::Types::SubmissionPublicationTransitionType + end +end diff --git a/app/graphql/types/submission_publication_transition_type.rb b/app/graphql/types/submission_publication_transition_type.rb new file mode 100644 index 00000000..75aaed2f --- /dev/null +++ b/app/graphql/types/submission_publication_transition_type.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Types + # @see SubmissionPublicationTransition + # @see ::Types::SubmissionPublicationTransitionConnectionType + # @see ::Types::SubmissionPublicationTransitionEdgeType + class SubmissionPublicationTransitionType < Types::AbstractModel + description <<~TEXT + A transition for a `SubmissionPublication`. + TEXT + + use_direct_connection_and_edge! + + implements ::Types::CommonTransitionType + + field :from_state, Types::SubmissionPublicationStateType, null: true do + description <<~TEXT + The state that the submission publication is transitioning from. This will be null if the submission target is being created. + TEXT + end + + field :to_state, Types::SubmissionPublicationStateType, null: false do + description <<~TEXT + The state that the submission publication is transitioning to. + TEXT + end + end +end diff --git a/app/graphql/types/submission_publication_type.rb b/app/graphql/types/submission_publication_type.rb new file mode 100644 index 00000000..c285c1c7 --- /dev/null +++ b/app/graphql/types/submission_publication_type.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Types + # @see SubmissionPublication + # @see ::Types::SubmissionPublicationConnectionType + # @see ::Types::SubmissionPublicationEdgeType + class SubmissionPublicationType < Types::AbstractModel + description <<~TEXT + The record of a `Submission`'s publication process. + TEXT + + use_direct_connection_and_edge! + + field :state, Types::SubmissionPublicationStateType, null: false do + description <<~TEXT + The state of the publication process. + TEXT + end + + field :submission, "Types::SubmissionType", null: false do + description <<~TEXT + The submission that is being published. + TEXT + end + + field :user, "Types::UserType", null: true do + description <<~TEXT + The user that initiated the publication process, if applicable. + TEXT + end + + field :transitions, resolver: Resolvers::SubmissionPublicationTransitionResolver, null: false do + description <<~TEXT + The transitions that the publication process has gone through. + TEXT + end + + load_association! :submission + + load_association! :user + end +end diff --git a/app/graphql/types/submission_review_state_type.rb b/app/graphql/types/submission_review_state_type.rb index 1135b836..64e4f993 100644 --- a/app/graphql/types/submission_review_state_type.rb +++ b/app/graphql/types/submission_review_state_type.rb @@ -12,6 +12,12 @@ class SubmissionReviewStateType < Types::BaseEnum TEXT end + value "REVISION_REQUESTED", value: "revision_requested" do + description <<~TEXT + The reviewer has requested revisions to the submission. + TEXT + end + value "APPROVED", value: "approved" do description <<~TEXT The reviewer has approved the submission. diff --git a/app/graphql/types/submission_status_type.rb b/app/graphql/types/submission_status_type.rb index 2b03cfbe..75e69ae6 100644 --- a/app/graphql/types/submission_status_type.rb +++ b/app/graphql/types/submission_status_type.rb @@ -22,6 +22,12 @@ class SubmissionStatusType < Types::BaseObject TEXT end + field :current, Boolean, null: false do + description <<~TEXT + Whether the submission is currently in this state. + TEXT + end + field :locked_state, Boolean, null: false do description <<~TEXT Whether the submission will be in a locked state (i.e. not mutable by the depositor). diff --git a/app/graphql/types/submission_target_type.rb b/app/graphql/types/submission_target_type.rb index 031a9554..21565384 100644 --- a/app/graphql/types/submission_target_type.rb +++ b/app/graphql/types/submission_target_type.rb @@ -18,6 +18,12 @@ class SubmissionTargetType < Types::AbstractModel TEXT end + field :depositor_agreement, "Types::DepositorAgreementType", null: true do + description <<~TEXT + The depositor agreement for this submission target and the current user, if one exists. + TEXT + end + field :deposit_mode, Types::SubmissionDepositModeType, null: false do description <<~TEXT The deposit mode of this submission target, which determines how deposits to it are handled. @@ -80,10 +86,18 @@ class SubmissionTargetType < Types::AbstractModel Whether or not the current user can manage reviewers for this submission target. TEXT + expose_authorization_rule :publish?, <<~TEXT + Whether or not the current user can publish submissions to this submission target. + TEXT + expose_authorization_rule :request_deposit_access?, <<~TEXT Whether or not the current user can request access to deposit to this submission target. TEXT + expose_authorization_rule :reset_all_agreements?, <<~TEXT + Whether or not the current user can reset all agreements for this submission target. + TEXT + expose_authorization_rule :review?, <<~TEXT Whether or not the current user can review this submission target. TEXT @@ -93,5 +107,10 @@ class SubmissionTargetType < Types::AbstractModel load_association! :schema_versions load_association! :submission_deposit_targets, as: :deposit_targets + + # @return [DepositorAgreement, nil] + def depositor_agreement + load_record_with(::DepositorAgreement, object.id, find_by: :submission_target_id, where: { user: context[:current_user].authenticated }) + end end end diff --git a/app/graphql/types/submission_type.rb b/app/graphql/types/submission_type.rb index ed49e833..21cceac4 100644 --- a/app/graphql/types/submission_type.rb +++ b/app/graphql/types/submission_type.rb @@ -71,6 +71,10 @@ class SubmissionType < Types::AbstractModel Whether or not the current user can migrate this submission. TEXT + expose_authorization_rule :publish?, <<~TEXT + Whether or not the current user can publish this submission. + TEXT + expose_authorization_rule :request_review?, <<~TEXT Whether or not the current user can request a review of this submission. TEXT diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb index a1956fb7..f8b13b5e 100644 --- a/app/graphql/types/user_type.rb +++ b/app/graphql/types/user_type.rb @@ -83,11 +83,24 @@ class UserType < Types::AbstractModel description "All access grants for this user on an item" 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. + + This is meant to be checked on the `Query.viewer`. + TEXT + + expose_authorization_rule :reset_password?, <<~TEXT + Whether the current user has permission to reset this user's password. + TEXT + + expose_authorization_rule :revalidate_instance?, <<~TEXT + Whether this user has permission to trigger revalidation of the entire frontend. + TEXT + # @see AnonymousInterface#system_slug_id # @see User#system_slug_id # @return [String] - def slug - object.system_slug_id - end + def slug = object.system_slug_id end end diff --git a/app/jobs/submission_batch_publications/finish_job.rb b/app/jobs/submission_batch_publications/finish_job.rb new file mode 100644 index 00000000..16c7df22 --- /dev/null +++ b/app/jobs/submission_batch_publications/finish_job.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module SubmissionBatchPublications + # A callback job for when the GoodJob batch is complete. + # + # @see SubmissionTargets::BatchPublisher + class FinishJob < ApplicationJob + queue_as :depositing + + # @param [GoodJob::Batch] batch + # @param [Hash] _context + # @return [void] + def perform(batch, _context) + sbp = batch.properties[:submission_batch_publication] + + # Quietly ignore if the batch publication is already finished + sbp.try(:transition_to, :finished) + end + end +end diff --git a/app/jobs/submission_publications/publish_job.rb b/app/jobs/submission_publications/publish_job.rb new file mode 100644 index 00000000..003954bf --- /dev/null +++ b/app/jobs/submission_publications/publish_job.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module SubmissionPublications + # @see SubmissionPublication#publish + # @see SubmissionPublications::Publish + # @see SubmissionPublications::Publisher + class PublishJob < ApplicationJob + queue_as :depositing + + # @param [SubmissionPublication] submission_publication + # @return [void] + def perform(submission_publication) + submission_publication.publish! + end + end +end diff --git a/app/models/concerns/entity_templating_support.rb b/app/models/concerns/entity_templating_support.rb index 3187ed58..49c5e600 100644 --- a/app/models/concerns/entity_templating_support.rb +++ b/app/models/concerns/entity_templating_support.rb @@ -47,21 +47,13 @@ module EntityTemplatingSupport call_operation("entities.derive_layout_definitions", entity: self) end - def has_layout_invalidations? - layout_invalidations.exists? - end + def has_layout_invalidations? = layout_invalidations.exists? - def has_missing_layouts? - missing_layout_instances.exists? - end + def has_missing_layouts? = missing_layout_instances.exists? - def has_missing_templates? - missing_template_instances.exists? - end + def has_missing_templates? = missing_template_instances.exists? - def has_no_layout_definitions_derived? - !entity_derived_layout_definitions.exists? - end + def has_no_layout_definitions_derived? = !entity_derived_layout_definitions.exists? # @see #invalidate_layouts # @see #invalidate_related_layouts @@ -87,19 +79,13 @@ def has_no_layout_definitions_derived? # @see Layouts::Disabled # @see Layouts::Disabler # @return [void] - def layout_invalidation_currently_disabled? - Layouts::Disabled.currently? - end + def layout_invalidation_currently_disabled? = Layouts::Disabled.currently? # @see ModelMutationSupport#in_graphql_mutation? - def layout_invalidation_disabled? - in_graphql_mutation? || layout_invalidation_currently_disabled? || force_disable_layout_invalidation - end + def layout_invalidation_disabled? = in_graphql_mutation? || layout_invalidation_currently_disabled? || force_disable_layout_invalidation # @return [String] - def render_lock_key - "render_lock/#{id}" - end + def render_lock_key = "render_lock/#{id}" # @see Entities::RenderLayout # @see Entities::LayoutRenderer @@ -131,9 +117,7 @@ def reprocess_layout(layout_kind) call_operation("entities.reprocess_layouts", self) end - def stale? - has_layout_invalidations? || has_missing_layouts? || has_missing_templates? || has_no_layout_definitions_derived? - end + def stale? = has_layout_invalidations? || has_missing_layouts? || has_missing_templates? || has_no_layout_definitions_derived? module ClassMethods # @api private diff --git a/app/models/depositor_agreement.rb b/app/models/depositor_agreement.rb new file mode 100644 index 00000000..4669c0ad --- /dev/null +++ b/app/models/depositor_agreement.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +# A depositor agreement marks the user's acceptance of the terms for submitting +# to a given {SubmissionTarget}. Its state is tracked so that we can reset it if +# the terms change and require users to re-accept. +# +# @see DepositorAgreementTransition +# @see DepositorAgreements::StateMachine +class DepositorAgreement < ApplicationRecord + include HasEphemeralSystemSlug + include TimestampScopes + include UsesStatesman + + pg_enum! :state, as: :depositor_agreement_state, allow_blank: false, default: "pending" + + has_state_machine! + + belongs_to :submission_target, inverse_of: :depositor_agreements + + belongs_to :user, inverse_of: :depositor_agreements + + # @see DepositorAgreements::Accept + # @see DepositorAgreements::Accepter + # @return [Dry::Monads::Success(DepositorAgreement)] + monadic_operation! def accept + call_operation("depositor_agreements.accept", depositor_agreement: self) + end + + # @see DepositorAgreements::Reset + # @see DepositorAgreements::Resetter + # @return [Dry::Monads::Success(DepositorAgreement)] + monadic_operation! def reset + call_operation("depositor_agreements.reset", depositor_agreement: self) + end + + class << self + # @param [User, AnonymousUser] user + # @return [ActiveRecord::Relation] + def owned_by(user) + # :nocov: + return none if user.blank? || user.anonymous? + # :nocov: + + where(user:) + end + + # Reset all accepted agreements to `pending`. + # + # @return [void] + def reset_all! + in_state(:accepted).find_each(&:reset!) + end + + # @param [User, AnonymousUser] user + # @return [ActiveRecord::Relation] + def reviewable_by(user) + # :nocov: + return none if user.blank? || user.anonymous? + + return all if user.has_global_admin_access? + # :nocov: + + where(submission_target: SubmissionTarget.reviewable_by(user)) + end + + # @param [User, AnonymousUser] user + # @return [ActiveRecord::Relation] + def visible_to(user) + return none if user.blank? || user.anonymous? + + return all if user.has_global_admin_access? + + owned = arel_expr_in_query(arel_table[:id], owned_by(user).select(:id)) + reviewable = arel_expr_in_query(arel_table[:id], reviewable_by(user).select(:id)) + + where(owned.or(reviewable)) + end + end +end diff --git a/app/models/depositor_agreement_transition.rb b/app/models/depositor_agreement_transition.rb new file mode 100644 index 00000000..a347a28b --- /dev/null +++ b/app/models/depositor_agreement_transition.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# @see DepositorAgreement +class DepositorAgreementTransition < ApplicationRecord + include Support::StatesmanHelpers::Transition + include CommonTransition + include HasEphemeralSystemSlug + include TimestampScopes + + stateful_enum! :depositor_agreement_state + + belongs_to :depositor_agreement, inverse_of: :depositor_agreement_transitions + belongs_to :user, inverse_of: :depositor_agreement_transitions, optional: true + + owner_association_name :depositor_agreement + + transitions_association_name :depositor_agreement_transitions +end diff --git a/app/models/depositor_request.rb b/app/models/depositor_request.rb index 65d3c462..66344f19 100644 --- a/app/models/depositor_request.rb +++ b/app/models/depositor_request.rb @@ -21,7 +21,9 @@ class DepositorRequest < ApplicationRecord # @see Access::Grant monadic_operation! def add_depositor - call_operation("access.grant", Role.fetch(:depositor), on: target_entity, to: user) + call_operation("access.grant", Role.fetch(:depositor), on: target_entity, to: user).bind do + submission_target.accept_agreement_for(user) + end end # @see Access::Revoke diff --git a/app/models/entity_link.rb b/app/models/entity_link.rb index d142d4cf..74c00db1 100644 --- a/app/models/entity_link.rb +++ b/app/models/entity_link.rb @@ -48,7 +48,31 @@ class EntityLink < ApplicationRecord validates :scope, inclusion: { in: Links::Types::SCOPE_VALUES } validates :target_id, uniqueness: { scope: %i[source_type source_id target_type] } - after_save :refresh_source_orderings! + after_save :refresh_source_orderings!, unless: :maintenance_mode? + + # We want to skip refreshing source orderings during link maintenance, + # since this will already happen as part of the entity's maintenance. + # + # @see #check! + # @see Links::Maintain + # @return [Boolean] + attr_accessor :maintenance_mode + + alias maintenance_mode? maintenance_mode + + # @see Links::Maintain + # @return [void] + def check! + self.maintenance_mode = true + + if valid? + save! + else + destroy! + end + ensure + self.maintenance_mode = false + end # @api private # @return [String, nil] diff --git a/app/models/submission.rb b/app/models/submission.rb index 4c28672f..f92f9d4d 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -15,6 +15,8 @@ class Submission < ApplicationRecord has_many :submission_comments, -> { in_default_order }, dependent: :destroy, inverse_of: :submission + has_many :submission_publications, -> { in_recent_order }, inverse_of: :submission, dependent: :destroy + has_many :submission_reviews, inverse_of: :submission, dependent: :destroy belongs_to :submission_target, inverse_of: :submissions, optional: true @@ -39,6 +41,9 @@ class Submission < ApplicationRecord validates :entity_id, uniqueness: { scope: :entity_type, if: :entity_id? } + # @see Submissions::ConstructDraftEntity + # @see Submissions::DraftEntityConstructor + # @return [Dry::Monads::Result] monadic_operation! def construct_draft_entity call_operation("submissions.construct_draft_entity", self) end @@ -46,13 +51,24 @@ class Submission < ApplicationRecord # @return [] def available_transitions state_machine.allowed_transitions.map do |to_state| - Submissions::Status.new(self, to_state: to_state) + status_for(to_state) end end # @return [Submissions::Status] def current_status = Submissions::Status.new(self) + # @see Submissions::Publish + # @see Submissions::Publisher + # @return [Dry::Monads::Result] + monadic_operation! def publish(**options) + call_operation("submissions.publish", self, **options) + end + + # @param [String] to_state + # @return [Submissions::Status] + def status_for(to_state) = Submissions::Status.new(self, to_state:) + private # @return [void] diff --git a/app/models/submission_batch_publication.rb b/app/models/submission_batch_publication.rb new file mode 100644 index 00000000..391ee593 --- /dev/null +++ b/app/models/submission_batch_publication.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# A submission batch publication represents the actual process of publishing multiple {Submission}s +# within a single {SubmissionTarget}. +# +# It groups together multiple {SubmissionPublication}s, which each represent the publication of +# a single {Submission}. +# +# The user associated with the publication is the user that initiated the process, +# if applicable. In some cases, we may not have a user if we implement automated processes. +# +# @see SubmissionBatchPublications::StateMachine +# @see SubmissionBatchPublicationTransition +class SubmissionBatchPublication < ApplicationRecord + include HasEphemeralSystemSlug + include TimestampScopes + include UsesStatesman + + pg_enum! :state, as: :submission_batch_publication_state, allow_blank: false, default: "pending" + + has_state_machine! + + belongs_to :submission_target, inverse_of: :submission_batch_publications + + belongs_to :user, inverse_of: :submission_batch_publications, optional: true + + has_many :submission_publications, -> { in_batch_order }, inverse_of: :submission_batch_publication, dependent: :restrict_with_error + + define_simple_lookups! :submission_target, :user + + class << self + # @param [User, AnonymousUser] user + # @return [ActiveRecord::Relation] + def visible_to(user) + # :nocov: + return none if user.blank? || user.anonymous? + + return all if user.has_global_admin_access? + # :nocov: + + where(submission_target: SubmissionTarget.visible_to(user)) + end + end +end diff --git a/app/models/submission_batch_publication_transition.rb b/app/models/submission_batch_publication_transition.rb new file mode 100644 index 00000000..9797a9d7 --- /dev/null +++ b/app/models/submission_batch_publication_transition.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# @see SubmissionBatchPublication +class SubmissionBatchPublicationTransition < ApplicationRecord + include Support::StatesmanHelpers::Transition + include CommonTransition + include HasEphemeralSystemSlug + include TimestampScopes + + stateful_enum! :submission_batch_publication_state + + belongs_to :submission_batch_publication, inverse_of: :submission_batch_publication_transitions + belongs_to :user, inverse_of: :submission_batch_publication_transitions, optional: true + + owner_association_name :submission_batch_publication + + transitions_association_name :submission_batch_publication_transitions +end diff --git a/app/models/submission_publication.rb b/app/models/submission_publication.rb new file mode 100644 index 00000000..adf7c629 --- /dev/null +++ b/app/models/submission_publication.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# A submission publication represents the actual process of publishing a {Submission}. +# Since publication can happen in the background and may fail in odd circumstances, we +# need a record outside of the {Submission} itself to capture that nuance. +# +# The user associated with the publication is the user that initiated the process, +# if applicable. In some cases, we may not have a user if we implement automated processes. +# +# @see SubmissionPublications::StateMachine +# @see SubmissionPublicationTransition +class SubmissionPublication < ApplicationRecord + include HasEphemeralSystemSlug + include TimestampScopes + include UsesStatesman + + pg_enum! :state, as: :submission_publication_state, allow_blank: false, default: "pending" + + has_state_machine! + + belongs_to :submission, inverse_of: :submission_publications + + belongs_to :user, inverse_of: :submission_publications, optional: true + + belongs_to :submission_batch_publication, inverse_of: :submission_publications, optional: true, counter_cache: :publications_count + + define_simple_lookups! :submission, :user, :submission_batch_publication + + scope :in_batch_order, -> { order(batch_position: :asc, created_at: :asc) } + + # A wrapper around {Submissions::Publisher}. + # + # @see Submission#publish + # @see SubmissionPublications::Publish + # @see SubmissionPublications::Publisher + # @return [Dry::Monads::Success(SubmissionPublication)] + monadic_operation! def publish + call_operation("submission_publications.publish", self) + end + + class << self + # @param [User, AnonymousUser] user + # @return [ActiveRecord::Relation] + def visible_to(user) + # :nocov: + return none if user.blank? || user.anonymous? + + return all if user.has_global_admin_access? + # :nocov: + + where(submission: Submission.visible_to(user)) + end + end +end diff --git a/app/models/submission_publication_transition.rb b/app/models/submission_publication_transition.rb new file mode 100644 index 00000000..ff8cecf3 --- /dev/null +++ b/app/models/submission_publication_transition.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# @see SubmissionPublication +class SubmissionPublicationTransition < ApplicationRecord + include Support::StatesmanHelpers::Transition + include CommonTransition + include HasEphemeralSystemSlug + include TimestampScopes + + stateful_enum! :submission_publication_state + + belongs_to :submission_publication, inverse_of: :submission_publication_transitions + belongs_to :user, inverse_of: :submission_publication_transitions, optional: true + + owner_association_name :submission_publication + + transitions_association_name :submission_publication_transitions +end diff --git a/app/models/submission_target.rb b/app/models/submission_target.rb index 59bd82e9..01d01d41 100644 --- a/app/models/submission_target.rb +++ b/app/models/submission_target.rb @@ -27,6 +27,10 @@ class SubmissionTarget < ApplicationRecord has_many :depositor_requests, dependent: :destroy, inverse_of: :submission_target + has_many :depositor_agreements, dependent: :destroy, inverse_of: :submission_target + + has_many :submission_batch_publications, dependent: :destroy, inverse_of: :submission_target + has_many :submission_deposit_targets, -> { includes(:entity) }, dependent: :destroy, inverse_of: :submission_target, autosave: true has_many :submission_target_reviewers, -> { in_default_order }, dependent: :destroy, inverse_of: :submission_target @@ -63,10 +67,37 @@ class SubmissionTarget < ApplicationRecord validate :must_have_schema_versions!, on: :opening + # @param [User] user + # @see DepositorAgreements::Accept + # @see DepositorAgreements::Accepter + # @return [Dry::Monads::Success(DepositorAgreement)] + monadic_operation! def accept_agreement_for(user) + call_operation("depositor_agreements.accept", submission_target: self, user:) + end + + # @param [User] user + # @return [DepositorAgreement] + def agreement_for(user) + depositor_agreements.find_or_initialize_by(user:) + end + + # @param [] submissions + # @param [User, nil] user + # @see SubmissionTargets::BatchPublisher + # @return [Dry::Monads::Success(SubmissionBatchPublication)] + monadic_operation! def batch_publish(*submissions, user: nil) + call_operation("submission_targets.batch_publish", self, submissions.flatten, user:) + end + monadic_operation! def configure(**options) call_operation("submission_targets.configure", self, **options) end + # @param [User] user + def has_accepted_agreement?(user) + depositor_agreements.accepted.exists?(user:) if user.present? && user.authenticated? + end + def missing_descendant_targets? = descendant_deposit? && !submission_deposit_targets.descendant_deposit.exists? private @@ -143,10 +174,22 @@ def reviewable_by(user) with_contextual_action_for(user, "self.review") end + def visible_to(user) + # :nocov: + return none if user.blank? || user.anonymous? + + return all if user.has_global_admin_access? + # :nocov: + + actions = %w[self.read self.review self.deposit self.update] + + with_contextual_action_for(user, actions) + end + private # @param [User] user - # @param [String] action + # @param [String, ] action # @return [ActiveRecord::Relation] def with_contextual_action_for(user, action) search_scope = SubmissionTarget.joins(:contextual_single_permissions) diff --git a/app/models/user.rb b/app/models/user.rb index 8e1e65da..f1277003 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -20,14 +20,26 @@ class User < ApplicationRecord has_many :user_groups, through: :user_group_memberships + has_many :depositor_agreements, inverse_of: :user, dependent: :destroy + + has_many :depositor_agreement_transitions, dependent: :nullify, inverse_of: :user + has_many :depositor_requests, inverse_of: :user, dependent: :restrict_with_error has_many :depositor_request_transitions, dependent: :nullify, inverse_of: :user has_many :submissions, inverse_of: :user, dependent: :restrict_with_error + has_many :submission_batch_publications, inverse_of: :user, dependent: :nullify + + has_many :submission_batch_publication_transitions, inverse_of: :user, dependent: :nullify + has_many :submission_comments, inverse_of: :user, dependent: :restrict_with_error + has_many :submission_publications, inverse_of: :user, dependent: :nullify + + has_many :submission_publication_transitions, inverse_of: :user, dependent: :nullify + has_many :submission_reviews, inverse_of: :user, dependent: :restrict_with_error has_many :submission_review_transitions, inverse_of: :user, dependent: :nullify @@ -59,6 +71,9 @@ def anonymous? = false def authenticated? = true + # @return [User] + def authenticated = self + # @param [HierarchicalEntity] entity # @return [ContextualPermission, nil] def contextual_permissions_for(entity) = ContextualPermission.fetch(self, entity) diff --git a/app/operations/depositor_agreements/accept.rb b/app/operations/depositor_agreements/accept.rb new file mode 100644 index 00000000..847c1686 --- /dev/null +++ b/app/operations/depositor_agreements/accept.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module DepositorAgreements + # @see DepositorAgreements::Accepter + class Accept < Support::SimpleServiceOperation + service_klass DepositorAgreements::Accepter + end +end diff --git a/app/operations/depositor_agreements/reset.rb b/app/operations/depositor_agreements/reset.rb new file mode 100644 index 00000000..a1b100bf --- /dev/null +++ b/app/operations/depositor_agreements/reset.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module DepositorAgreements + # @see DepositorAgreements::Resetter + class Reset < Support::SimpleServiceOperation + service_klass DepositorAgreements::Resetter + end +end diff --git a/app/operations/links/maintain.rb b/app/operations/links/maintain.rb index ce78a1ca..7235e90f 100644 --- a/app/operations/links/maintain.rb +++ b/app/operations/links/maintain.rb @@ -1,32 +1,18 @@ # frozen_string_literal: true module Links + # @see EntityLink#check! class Maintain - include Dry::Monads[:do, :result] - include MonadicPersistence - - prepend TransactionalCall + include Dry::Monads[:result] + # @param [HierarchicalEntity] entity + # @return [Dry::Monads::Success(void)] def call(entity) EntityLink.by_source_or_target(entity).find_each do |link| - yield check link + link.check! end - Success nil - end - - private - - # @param [EntityLink] link - # @return [Dry::Monads::Result] - def check(link) - if link.valid? - monadic_save link - else - link.destroy! - - Success nil - end + Success() end end end diff --git a/app/operations/mutations/contracts/depositor_agreement_accept.rb b/app/operations/mutations/contracts/depositor_agreement_accept.rb new file mode 100644 index 00000000..432c5527 --- /dev/null +++ b/app/operations/mutations/contracts/depositor_agreement_accept.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Mutations + module Contracts + # @see Mutations::DepositorAgreementAccept + # @see Mutations::Operations::DepositorAgreementAccept + class DepositorAgreementAccept < MutationOperations::Contract + json do + required(:submission_target).value(:submission_target) + end + end + end +end diff --git a/app/operations/mutations/contracts/depositor_agreement_reset.rb b/app/operations/mutations/contracts/depositor_agreement_reset.rb new file mode 100644 index 00000000..17277078 --- /dev/null +++ b/app/operations/mutations/contracts/depositor_agreement_reset.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Mutations + module Contracts + # @see Mutations::DepositorAgreementReset + # @see Mutations::Operations::DepositorAgreementReset + class DepositorAgreementReset < MutationOperations::Contract + json do + required(:submission_target).value(:submission_target) + required(:user).value(:user) + end + end + end +end diff --git a/app/operations/mutations/contracts/depositor_agreement_reset_all.rb b/app/operations/mutations/contracts/depositor_agreement_reset_all.rb new file mode 100644 index 00000000..82e7097d --- /dev/null +++ b/app/operations/mutations/contracts/depositor_agreement_reset_all.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Mutations + module Contracts + # @see Mutations::DepositorAgreementResetAll + # @see Mutations::Operations::DepositorAgreementResetAll + class DepositorAgreementResetAll < MutationOperations::Contract + json do + required(:submission_target).value(:submission_target) + end + end + end +end diff --git a/app/operations/mutations/contracts/submission_batch_publish.rb b/app/operations/mutations/contracts/submission_batch_publish.rb new file mode 100644 index 00000000..d0a90dec --- /dev/null +++ b/app/operations/mutations/contracts/submission_batch_publish.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Mutations + module Contracts + # @see Mutations::SubmissionBatchPublish + # @see Mutations::Operations::SubmissionBatchPublish + class SubmissionBatchPublish < MutationOperations::Contract + json do + required(:submission_target).value(:submission_target) + required(:submissions).array(:submission) + end + + rule(:submissions).each do + key.failure(:mismatched_batch_submission_target) if value.submission_target != values[:submission_target] + key.failure(:must_be_publishable) unless value.can_transition_to?(:published) + end + end + end +end diff --git a/app/operations/mutations/contracts/submission_publish.rb b/app/operations/mutations/contracts/submission_publish.rb new file mode 100644 index 00000000..5e216740 --- /dev/null +++ b/app/operations/mutations/contracts/submission_publish.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Mutations + module Contracts + # @see Mutations::SubmissionPublish + # @see Mutations::Operations::SubmissionPublish + class SubmissionPublish < MutationOperations::Contract + json do + required(:submission).value(:submission) + end + + rule(:submission) do + key.failure(:must_be_publishable) unless value.can_transition_to?(:published) + end + end + end +end diff --git a/app/operations/mutations/operations/depositor_agreement_accept.rb b/app/operations/mutations/operations/depositor_agreement_accept.rb new file mode 100644 index 00000000..90826b2b --- /dev/null +++ b/app/operations/mutations/operations/depositor_agreement_accept.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Mutations + module Operations + # @see Mutations::DepositorAgreementAccept + class DepositorAgreementAccept + include MutationOperations::Base + + use_contract! :depositor_agreement_accept + + authorizes! :depositor_agreement, with: :accept? + + # @param [DepositorAgreement] depositor_agreement + # @return [void] + def call(depositor_agreement:, **) + with_attached_result! :depositor_agreement, depositor_agreement.accept + end + + before_prepare def fetch_depositor_agreement! + args => { submission_target: } + + args[:depositor_agreement] = submission_target.agreement_for(current_user.authenticated) + end + end + end +end diff --git a/app/operations/mutations/operations/depositor_agreement_reset.rb b/app/operations/mutations/operations/depositor_agreement_reset.rb new file mode 100644 index 00000000..309b580c --- /dev/null +++ b/app/operations/mutations/operations/depositor_agreement_reset.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Mutations + module Operations + # @see Mutations::DepositorAgreementReset + class DepositorAgreementReset + include MutationOperations::Base + + use_contract! :depositor_agreement_reset + + authorizes! :depositor_agreement, with: :reset? + + # @param [DepositorAgreement] depositor_agreement + # @return [void] + def call(depositor_agreement:, **) + with_attached_result! :depositor_agreement, depositor_agreement.reset + end + + before_prepare def fetch_depositor_agreement! + args => { submission_target:, user: } + + args[:depositor_agreement] = submission_target.agreement_for(user) + end + end + end +end diff --git a/app/operations/mutations/operations/depositor_agreement_reset_all.rb b/app/operations/mutations/operations/depositor_agreement_reset_all.rb new file mode 100644 index 00000000..3df485b1 --- /dev/null +++ b/app/operations/mutations/operations/depositor_agreement_reset_all.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Mutations + module Operations + # @see Mutations::DepositorAgreementResetAll + class DepositorAgreementResetAll + include MutationOperations::Base + + use_contract! :depositor_agreement_reset_all + + authorizes! :submission_target, with: :reset_all_agreements? + + # @param [{ Symbol => Object }] args + # @return [void] + def call(submission_target:, **) + submission_target.depositor_agreements.reset_all! + + attach! :submission_target, submission_target + end + end + end +end diff --git a/app/operations/mutations/operations/submission_batch_publish.rb b/app/operations/mutations/operations/submission_batch_publish.rb new file mode 100644 index 00000000..34ea1737 --- /dev/null +++ b/app/operations/mutations/operations/submission_batch_publish.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Mutations + module Operations + # @see Mutations::SubmissionBatchPublish + class SubmissionBatchPublish + include MutationOperations::Base + + use_contract! :submission_batch_publish + + authorizes! :submission_target, with: :publish? + authorizes! :submissions, with: :publish?, each: true + + # @param [SubmissionTarget] submission_target + # @param [] submissions + # @return [void] + def call(submission_target:, submissions:, **) + result = submission_target.batch_publish(*submissions, user: current_user) + + with_attached_result! :submission_batch_publication, result + + attach! :submission_target, submission_target.reload + end + end + end +end diff --git a/app/operations/mutations/operations/submission_change_state.rb b/app/operations/mutations/operations/submission_change_state.rb index 854a88d6..b0dcd4ea 100644 --- a/app/operations/mutations/operations/submission_change_state.rb +++ b/app/operations/mutations/operations/submission_change_state.rb @@ -22,7 +22,7 @@ def call(submission:, to_state:, **) before_prepare def build_status! args => { submission:, to_state: } - args[:submission_status] = Submissions::Status.new(submission, to_state:) + args[:submission_status] = submission.status_for(to_state) end end end diff --git a/app/operations/mutations/operations/submission_create.rb b/app/operations/mutations/operations/submission_create.rb index 08cdeeca..19829d72 100644 --- a/app/operations/mutations/operations/submission_create.rb +++ b/app/operations/mutations/operations/submission_create.rb @@ -10,6 +10,8 @@ class SubmissionCreate authorizes! :submission_target, with: :deposit? + authorizes! :submission, with: :create? + # @param [Submission] submission # @param [Hash] attrs # @return [void] @@ -22,9 +24,7 @@ def call(submission:, **attrs) before_prepare def initialize_submission! args => { submission_target:, schema_version:, parent_entity: } - attrs = { submission_target:, schema_version:, parent_entity: } - - attrs[:user] = current_user unless current_user.anonymous? + attrs = { submission_target:, schema_version:, parent_entity:, user: current_user.authenticated } args[:submission] = Submission.new(**attrs) end diff --git a/app/operations/mutations/operations/submission_publish.rb b/app/operations/mutations/operations/submission_publish.rb new file mode 100644 index 00000000..89af7b4f --- /dev/null +++ b/app/operations/mutations/operations/submission_publish.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Mutations + module Operations + # @see Mutations::SubmissionPublish + class SubmissionPublish + include MutationOperations::Base + + use_contract! :submission_publish + + authorizes! :submission, with: :publish? + + # @param [Submission] submission + # @return [void] + def call(submission:, **) + with_attached_result! :submission_publication, submission.publish(user: current_user) + + attach! :submission, submission.reload + + attach! :entity, submission.reload_entity + end + end + end +end diff --git a/app/operations/mutations/operations/submission_target_configure.rb b/app/operations/mutations/operations/submission_target_configure.rb index adacca38..d38eef35 100644 --- a/app/operations/mutations/operations/submission_target_configure.rb +++ b/app/operations/mutations/operations/submission_target_configure.rb @@ -14,9 +14,7 @@ class SubmissionTargetConfigure # @param [{ Symbol => Object }] attrs # @return [void] def call(configurable:, **attrs) - authorize configurable, :update? - - result = call_operation("submission_targets.configure", configurable, **attrs) + result = configurable.configure_submission_target(**attrs) with_attached_result!(:submission_target, result) end diff --git a/app/operations/submission_publications/publish.rb b/app/operations/submission_publications/publish.rb new file mode 100644 index 00000000..15f5257d --- /dev/null +++ b/app/operations/submission_publications/publish.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module SubmissionPublications + # @see SubmissionPublications::Publisher + class Publish < Support::SimpleServiceOperation + service_klass SubmissionPublications::Publisher + end +end diff --git a/app/operations/submission_targets/batch_publish.rb b/app/operations/submission_targets/batch_publish.rb new file mode 100644 index 00000000..6028097b --- /dev/null +++ b/app/operations/submission_targets/batch_publish.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module SubmissionTargets + # @see SubmissionTargets::BatchPublisher + class BatchPublish < Support::SimpleServiceOperation + service_klass SubmissionTargets::BatchPublisher + end +end diff --git a/app/operations/testing/add_tree_ordering.rb b/app/operations/testing/add_tree_ordering.rb deleted file mode 100644 index 6ef781cd..00000000 --- a/app/operations/testing/add_tree_ordering.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module Testing - # @api private - class AddTreeOrdering - include Dry::Monads[:do, :result] - include MonadicPersistence - - # @param [SchemaInstance] entity - # @return [Ordering] - def call(entity) - adder = Testing::TreeOrderingAdder.new - - adder.call entity - end - end -end diff --git a/app/operations/testing/make_community_manager.rb b/app/operations/testing/make_community_manager.rb deleted file mode 100644 index b4de980f..00000000 --- a/app/operations/testing/make_community_manager.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module Testing - class MakeCommunityManager - include Dry::Monads[:do, :result] - include MeruAPI::Deps[grant_access: "access.grant"] - include MonadicPersistence - - def call(user) - user.global_access_control_list.communities = { - manage_access: true, - read: true, - create: true, - update: true, - delete: true, - assets: { - read: true, - create: true, - update: true, - delete: true - } - } - - yield monadic_save user - - manager = Role.fetch "Manager" - - Community.find_each do |community| - yield grant_access.call manager, on: community, to: user - end - - Success true - end - end -end diff --git a/app/operations/testing/populate_test_layouts_and_templates.rb b/app/operations/testing/populate_test_layouts_and_templates.rb deleted file mode 100644 index 7622a7cc..00000000 --- a/app/operations/testing/populate_test_layouts_and_templates.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -module Testing - class PopulateTestLayoutsAndTemplates - include Dry::Monads[:result, :do] - - include MonadicPersistence - - def call - SchemaVersion.find_each(&:populate_root_layouts!) - - Community.find_each(&:invalidate_layouts!) - Collection.find_each(&:invalidate_layouts!) - Item.find_each(&:invalidate_layouts!) - - Rendering::ProcessLayoutInvalidationsJob.perform_later - - Success() - end - end -end diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index 755cad0f..8e632c4c 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -95,6 +95,8 @@ def always_readable? = self.class.always_readable def anonymous? = user.anonymous? + def authenticated? = user.authenticated? + def has_any_access_management_permissions? = user.can_manage_access_globally? || user.can_manage_access_contextually? # Whether the user has global admin access diff --git a/app/policies/depositor_agreement_policy.rb b/app/policies/depositor_agreement_policy.rb new file mode 100644 index 00000000..646fad73 --- /dev/null +++ b/app/policies/depositor_agreement_policy.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# @see DepositorAgreement +class DepositorAgreementPolicy < ApplicationPolicy + pre_check :deny_anonymous! + + pre_check :allow_any_admin!, only: %i[read? show? index? reset?] + + def create? = false + + def update? = false + + def destroy? = false + + def read? = deposit? || review? + + def show? = read? + + def accept? = record.pending? && record_owned_by_current_user? && deposit? + + # Only admins can do this for now. + def reset? = false + + private + + def deposit? = allowed_to?(:deposit?, record.submission_target) + + def review? = allowed_to?(:review?, record.submission_target) + + # @param [ActiveRecord::Relation] relation + def resolve_scope_for_authenticated(relation) + relation.visible_to(user) + end +end diff --git a/app/policies/depositor_agreement_transition_policy.rb b/app/policies/depositor_agreement_transition_policy.rb new file mode 100644 index 00000000..a9d86c36 --- /dev/null +++ b/app/policies/depositor_agreement_transition_policy.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# @see DepositorAgreement +# @see DepositorAgreementTransition +# @see Types::DepositorAgreementTransitionType +class DepositorAgreementTransitionPolicy < AbstractTransitionPolicy + # Inherits all the default transition logic. +end diff --git a/app/policies/submission_batch_publication_policy.rb b/app/policies/submission_batch_publication_policy.rb new file mode 100644 index 00000000..0c00855d --- /dev/null +++ b/app/policies/submission_batch_publication_policy.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# @see SubmissionBatchPublication +class SubmissionBatchPublicationPolicy < ApplicationPolicy + pre_check :deny_anonymous! + + pre_check :allow_any_admin!, only: %i[read? show? index?] + + def read? = deposit? || review? + + def show? = read? + + def create? = false + + def update? = false + + def destroy? = false + + private + + def deposit? = allowed_to?(:deposit?, record.submission_target) + + def review? = allowed_to?(:review?, record.submission_target) + + # @param [ActiveRecord::Relation] relation + def resolve_scope_for_authenticated(relation) + relation.visible_to(user) + end +end diff --git a/app/policies/submission_batch_publication_transition_policy.rb b/app/policies/submission_batch_publication_transition_policy.rb new file mode 100644 index 00000000..f65c9313 --- /dev/null +++ b/app/policies/submission_batch_publication_transition_policy.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# @see SubmissionBatchPublication +# @see SubmissionBatchPublicationTransition +# @see Types::SubmissionBatchPublicationTransitionType +class SubmissionBatchPublicationTransitionPolicy < AbstractTransitionPolicy + # Inherits all the default transition logic. +end diff --git a/app/policies/submission_policy.rb b/app/policies/submission_policy.rb index 4a8ad7a4..59899e30 100644 --- a/app/policies/submission_policy.rb +++ b/app/policies/submission_policy.rb @@ -6,43 +6,41 @@ class SubmissionPolicy < ApplicationPolicy pre_check :allow_any_admin!, except: :destroy? - def read? = allowed_to?(:update?, record.submission_target) || deposit? || review? || record_owned_by_current_user? + def read? = manage_target? || deposit? || review? || record_owned_by_current_user? def show? = read? def index? = read? - def review? = allowed_to?(:review?, record.submission_target) - def alter_schema_version? = manage_target? && !published? def comment? = deposit? || review? || record_owned_by_current_user? + def migrate? = manage_target? && !published? + + # @see SubmissionTargetPolicy#publish? + def publish? = allowed_to?(:publish?, record.submission_target) + def request_review? = deposit? || review? - def migrate? = manage_target? && !published? + def review? = allowed_to?(:review?, record.submission_target) - def create? = deposit? + def create? = deposit? && has_accepted_agreement? - def update? = deposit? + def update? = record_owned_by_current_user? # Submissions cannot be destroyed, only rejected. def destroy? = false - # @!group - private - # @return [Submissions::Status] - def current_status - @current_status ||= Submissions::Status.new(record) - end - def deposit? = allowed_to?(:deposit?, record.submission_target) + def has_accepted_agreement? = authenticated? && record.submission_target.has_accepted_agreement?(user) + def manage_target? = allowed_to?(:update?, record.submission_target) - def published? = record.state == "published" + def published? = record.published? # @param [ActiveRecord::Relation] relation def resolve_scope_for_authenticated(relation) diff --git a/app/policies/submission_publication_policy.rb b/app/policies/submission_publication_policy.rb new file mode 100644 index 00000000..039d3fde --- /dev/null +++ b/app/policies/submission_publication_policy.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# @see SubmissionPublication +class SubmissionPublicationPolicy < ApplicationPolicy + pre_check :deny_anonymous! + + pre_check :allow_any_admin!, only: %i[read? show? index?] + + def read? = allowed_to?(:read?, record.submission) + + def show? = read? + + def create? = false + + def update? = false + + def destroy? = false + + private + + # @param [ActiveRecord::Relation] relation + def resolve_scope_for_authenticated(relation) + relation.visible_to(user) + end +end diff --git a/app/policies/submission_publication_transition_policy.rb b/app/policies/submission_publication_transition_policy.rb new file mode 100644 index 00000000..04d4c6e8 --- /dev/null +++ b/app/policies/submission_publication_transition_policy.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# @see SubmissionPublication +# @see SubmissionPublicationTransition +# @see Types::SubmissionPublicationTransitionType +class SubmissionPublicationTransitionPolicy < AbstractTransitionPolicy + # Inherits all the default transition logic. +end diff --git a/app/policies/submission_status_policy.rb b/app/policies/submission_status_policy.rb deleted file mode 100644 index 46d49d21..00000000 --- a/app/policies/submission_status_policy.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -# @see Submissions::Status -class SubmissionStatusPolicy < ApplicationPolicy - pre_check :deny_anonymous! - - pre_check :allow_managers!, only: %i[update? transition?] - - delegate :mutable_state?, :locked_state?, to: :record, prefix: :in - - def deposit? = allowed_to?(:deposit?, record.submission_target) - - def manage_target? = allowed_to?(:update?, record.target_entity) - - def update? = in_mutable_state? - - def transition? = deposit? - - private - - # @return [void] - def allow_managers! - allow! if in_locked_state? && manage_target? - deny! if in_locked_state? - end -end diff --git a/app/policies/submission_target_policy.rb b/app/policies/submission_target_policy.rb index 04504909..e23fb899 100644 --- a/app/policies/submission_target_policy.rb +++ b/app/policies/submission_target_policy.rb @@ -20,8 +20,12 @@ def deposit? = open? && allowed_to?(:deposit?, record.entity) def manage_reviewers? = update? + def publish? = update? + def request_deposit_access? = open? && !deposit? && no_deposit_request_exists? + def reset_all_agreements? = update? + def review? = allowed_to?(:review?, record.entity) relation_scope do |relation| diff --git a/app/policies/submissions/status_policy.rb b/app/policies/submissions/status_policy.rb new file mode 100644 index 00000000..76ee0d3a --- /dev/null +++ b/app/policies/submissions/status_policy.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Submissions + # @see Submissions::Status + class StatusPolicy < ApplicationPolicy + pre_check :deny_anonymous! + + pre_check :deny_published! + + pre_check :guard_locked!, only: %i[update? transition?] + + delegate :mutable_state?, :locked_state?, to: :record, prefix: :in + + delegate :to_state, to: :record + + def update? = in_mutable_state? + + def transition? = deposit? + + private + + def can_transition_locked? = in_locked_state? && manage? + + # Whether the current user has permission to deposit on the {SubmissionTarget}. + # + # @see SubmissionTargetPolicy#deposit? + def deposit? = allowed_to?(:deposit?, record.submission_target) + + # Published states must be handled through the publish mutations. + # + # @return [void] + def deny_published! + deny! if record.any_published? + end + + # @return [void] + def guard_locked! + allow! if can_transition_locked? + + deny! if in_locked_state? + end + + # Whether the current user has permission to update the submission target entity. + # + # @see HierarchicalEntityPolicy#update? + def manage? = allowed_to?(:update?, record.target_entity) + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 8aaa31e8..9a8be5d1 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -3,17 +3,19 @@ # Presently, user creation and destruction is managed in Keycloak # and cannot be handled directly in Meru. Permissions reflect this. class UserPolicy < ApplicationPolicy - pre_check :allow_any_admin!, except: %i[create? destroy?] - pre_check :deny_anonymous!, only: %i[update? reset_password? revalidate_instance?] + pre_check :allow_any_admin!, except: %i[create? destroy? receive_review_requests? revalidate_instance?] + pre_check :deny_anonymous!, only: %i[update? receive_review_requests? reset_password? revalidate_instance?] pre_check :allow_authenticated_self_action!, only: %i[read? update? reset_password?] def read? = record.anonymous? || has_allowed_action?("users.read") || has_any_access_management_permissions? def update? = has_allowed_action?("users.update") + def receive_review_requests? = admin_record? || record.submission_target_reviewers.exists? + def reset_password? = has_allowed_action?("users.update") - def revalidate_instance? = has_admin? + def revalidate_instance? = admin_record? def destroy? = false @@ -24,6 +26,8 @@ def allow_authenticated_self_action! allow! if authenticated_self_action? end + def admin_record? = record.has_global_admin_access? + def authenticated_self_action? = record == user def resolve_scope_for_authenticated(relation) diff --git a/app/services/access/types.rb b/app/services/access/types.rb index d61a4d52..70a7be83 100644 --- a/app/services/access/types.rb +++ b/app/services/access/types.rb @@ -3,9 +3,7 @@ module Access # Types for Access-related operations and services. module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace # @see AccessGrant AccessGrant = ModelInstance("AccessGrant") diff --git a/app/services/analytics/date_filter.rb b/app/services/analytics/date_filter.rb index 7ff002a1..67c62dc5 100644 --- a/app/services/analytics/date_filter.rb +++ b/app/services/analytics/date_filter.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Analytics - class DateFilter < Shared::FlexibleStruct + class DateFilter < ::Support::FlexibleStruct include Dry::Core::Memoizable attribute? :start_date, Analytics::Types::Date.optional diff --git a/app/services/analytics/event_count_result.rb b/app/services/analytics/event_count_result.rb index f393faba..d17e9604 100644 --- a/app/services/analytics/event_count_result.rb +++ b/app/services/analytics/event_count_result.rb @@ -2,7 +2,7 @@ module Analytics # @see Types::AnalyticsEventCountResultType - class EventCountResult < Shared::FlexibleStruct + class EventCountResult < ::Support::FlexibleStruct attribute :date, Analytics::Types::Date attribute? :time, Analytics::Types::Time.optional attribute :count, Analytics::Types::Integer.default(0) diff --git a/app/services/analytics/event_count_summary.rb b/app/services/analytics/event_count_summary.rb index 90fac220..98e802c0 100644 --- a/app/services/analytics/event_count_summary.rb +++ b/app/services/analytics/event_count_summary.rb @@ -2,7 +2,7 @@ module Analytics # @see Types::AnalyticsEventCountSummaryType - class EventCountSummary < Shared::FlexibleStruct + class EventCountSummary < ::Support::FlexibleStruct attribute :min_date, Analytics::Types::Date.optional attribute :max_date, Analytics::Types::Date.optional diff --git a/app/services/analytics/region_count_result.rb b/app/services/analytics/region_count_result.rb index 4856ccad..f96e3c4b 100644 --- a/app/services/analytics/region_count_result.rb +++ b/app/services/analytics/region_count_result.rb @@ -2,7 +2,7 @@ module Analytics # @see Types::AnalyticsRegionCountResultType - class RegionCountResult < Shared::FlexibleStruct + class RegionCountResult < ::Support::FlexibleStruct attribute? :country_code, Analytics::Types::String.default("$unknown$") attribute? :region_code, Analytics::Types::String.default("$unknown$") attribute? :count, Analytics::Types::Integer.default(0) diff --git a/app/services/analytics/region_count_summary.rb b/app/services/analytics/region_count_summary.rb index 9663f9d9..0ae25d2e 100644 --- a/app/services/analytics/region_count_summary.rb +++ b/app/services/analytics/region_count_summary.rb @@ -2,7 +2,7 @@ module Analytics # @see Types::AnalyticsRegionCountSummaryType - class RegionCountSummary < Shared::FlexibleStruct + class RegionCountSummary < ::Support::FlexibleStruct attribute :results, Analytics::Types::Array.of(Analytics::RegionCountResult) attribute :unfiltered_total, Analytics::Types::Integer diff --git a/app/services/analytics/types.rb b/app/services/analytics/types.rb index 10d6c737..1b1b0f5c 100644 --- a/app/services/analytics/types.rb +++ b/app/services/analytics/types.rb @@ -3,7 +3,7 @@ module Analytics # Types for working with analytics services module Types - include Dry.Types + extend ::Support::Typespace Entity = Entities::Types::Entity diff --git a/app/services/app_types.rb b/app/services/app_types.rb index 726d1a56..b9458a83 100644 --- a/app/services/app_types.rb +++ b/app/services/app_types.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module AppTypes - include Dry.Types - - extend Shared::EnhancedTypes + extend ::Support::Typespace ISSN_PATTERN = /\A\d{4}-\d{4}\z/ diff --git a/app/services/assets/types.rb b/app/services/assets/types.rb index 4b48ce1f..c551c201 100644 --- a/app/services/assets/types.rb +++ b/app/services/assets/types.rb @@ -2,9 +2,7 @@ module Assets module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Kind = ApplicationRecord.dry_pg_enum(:asset_kind, default: "unknown").fallback("unknown") end diff --git a/app/services/cache_warmers/types.rb b/app/services/cache_warmers/types.rb index 5477a9b3..0cdfb521 100644 --- a/app/services/cache_warmers/types.rb +++ b/app/services/cache_warmers/types.rb @@ -2,9 +2,7 @@ module CacheWarmers module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace CacheWarmer = ModelInstance("CacheWarmer") end diff --git a/app/services/collections/types.rb b/app/services/collections/types.rb index d7f8d6a7..b073e1c4 100644 --- a/app/services/collections/types.rb +++ b/app/services/collections/types.rb @@ -2,9 +2,7 @@ module Collections module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Collection = ModelInstance("Collection") end diff --git a/app/services/communities/types.rb b/app/services/communities/types.rb index e3f0c1fa..bf2af93c 100644 --- a/app/services/communities/types.rb +++ b/app/services/communities/types.rb @@ -2,9 +2,7 @@ module Communities module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Community = ModelInstance("Community") end diff --git a/app/services/contribution_roles/types.rb b/app/services/contribution_roles/types.rb index a3b608a0..a136bbd7 100644 --- a/app/services/contribution_roles/types.rb +++ b/app/services/contribution_roles/types.rb @@ -2,9 +2,7 @@ module ContributionRoles module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Contributable = ModelInstance("Collection") | ModelInstance("Item") diff --git a/app/services/contributions/types.rb b/app/services/contributions/types.rb index 522dc4b2..d94ac110 100644 --- a/app/services/contributions/types.rb +++ b/app/services/contributions/types.rb @@ -2,9 +2,7 @@ module Contributions module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Contributable = ModelInstance("Collection") | ModelInstance("Item") diff --git a/app/services/contributors/types.rb b/app/services/contributors/types.rb index a08a494b..595889da 100644 --- a/app/services/contributors/types.rb +++ b/app/services/contributors/types.rb @@ -2,9 +2,7 @@ module Contributors module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace ORCID_FORMAT = %r,\Ahttps://orcid.org/(?\d{4}(?:-\d{4}){3})\z, diff --git a/app/services/controlled_vocabularies/types.rb b/app/services/controlled_vocabularies/types.rb index fc3affe3..797c4c61 100644 --- a/app/services/controlled_vocabularies/types.rb +++ b/app/services/controlled_vocabularies/types.rb @@ -2,11 +2,9 @@ module ControlledVocabularies module Types - include Dry.Types + extend ::Support::Typespace include Dry::Core::Constants - extend Support::EnhancedTypes - NAMESPACE_PATTERN = /\A(?:[a-z]\w+[a-z0-9])(?:\.(?:[a-z]\w+[a-z0-9]))+\z/ IDENTIFIER_PATTERN = /\A(?:[a-z](?:[_-]?[a-z0-9])+)\z/ PROVIDES_PATTERN = /\A(?:[a-z](?:[_-]?[a-z0-9])+)\z/ diff --git a/app/services/depositor_agreements/abstract_state_enforcer.rb b/app/services/depositor_agreements/abstract_state_enforcer.rb new file mode 100644 index 00000000..ea038c58 --- /dev/null +++ b/app/services/depositor_agreements/abstract_state_enforcer.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module DepositorAgreements + # @abstract + class AbstractStateEnforcer < Support::HookBased::Actor + extend Dry::Core::ClassAttributes + + include Dry::Initializer[undefined: false].define -> do + option :depositor_agreement, Types::DepositorAgreement.optional, as: :provided_depositor_agreement, optional: true + option :submission_target, Types::SubmissionTarget, default: proc { depositor_agreement&.submission_target } + option :user, Types::User, default: proc { depositor_agreement&.user } + end + + defines :target_state, type: DepositorAgreements::Types::State + + target_state "pending" + + standard_execution! + + # @return [DepositorAgreement] + attr_reader :depositor_agreement + + delegate :in_state?, :transition_to!, to: :depositor_agreement + + # @return [Dry::Monads::Result] + def call + run_callbacks :execute do + yield prepare! + + yield enforce_state! + end + + Success depositor_agreement + end + + wrapped_hook! def prepare + @depositor_agreement = provided_depositor_agreement || submission_target.agreement_for(user) + + depositor_agreement.save! if depositor_agreement.new_record? + + super + end + + wrapped_hook! def enforce_state + transition_to!(target_state) unless in_state?(target_state) + + super + end + + private + + # @return ["pending", "accepted"] + def target_state = self.class.target_state + end +end diff --git a/app/services/depositor_agreements/accepter.rb b/app/services/depositor_agreements/accepter.rb new file mode 100644 index 00000000..33026d87 --- /dev/null +++ b/app/services/depositor_agreements/accepter.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module DepositorAgreements + # @see DepositorAgreements::Accept + class Accepter < AbstractStateEnforcer + target_state "accepted" + end +end diff --git a/app/services/depositor_agreements/resetter.rb b/app/services/depositor_agreements/resetter.rb new file mode 100644 index 00000000..dc1dfa92 --- /dev/null +++ b/app/services/depositor_agreements/resetter.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module DepositorAgreements + # @see DepositorAgreements::Reset + class Resetter < AbstractStateEnforcer + target_state "pending" + end +end diff --git a/app/services/depositor_agreements/state_machine.rb b/app/services/depositor_agreements/state_machine.rb new file mode 100644 index 00000000..ef5bf14b --- /dev/null +++ b/app/services/depositor_agreements/state_machine.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module DepositorAgreements + # @see SubmissionReview + # @see SubmissionReviewTransition + class StateMachine + include Statesman::Machine + include Support::StatesmanHelpers::Machine + + state :pending, initial: true + state :accepted + + transition from: :pending, to: :accepted + + transition from: :accepted, to: :pending + + after_transition do |depositor_agreement, transition| + depositor_agreement.update_column(:state, transition.to_state) + end + + after_transition to: :accepted do |depositor_agreement| + depositor_agreement.touch(:last_accepted_at) + end + end +end diff --git a/app/services/depositor_agreements/types.rb b/app/services/depositor_agreements/types.rb new file mode 100644 index 00000000..e5958510 --- /dev/null +++ b/app/services/depositor_agreements/types.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module DepositorAgreements + module Types + extend ::Support::Typespace + + DepositorAgreement = ModelInstance("DepositorAgreement") + + State = ApplicationRecord.dry_pg_enum("depositor_agreement_state", default: "pending").fallback("pending") + + SubmissionTarget = ModelInstance("SubmissionTarget") + + User = ModelInstance("User") + end +end diff --git a/app/services/entities/layout_renderer.rb b/app/services/entities/layout_renderer.rb index 353ca4ef..15be71f5 100644 --- a/app/services/entities/layout_renderer.rb +++ b/app/services/entities/layout_renderer.rb @@ -6,6 +6,8 @@ class LayoutRenderer < Support::HookBased::Actor include Dry::Initializer[undefined: false].define -> do param :entity, Layouts::Types::Entity + option :generation, Rendering::Types::Generation, default: proc { SecureRandom.uuid } + option :layout_kind, Layouts::Types::Kind end @@ -34,7 +36,7 @@ def call end wrapped_hook! def render_layout - yield layout_definition.render(entity) + yield layout_definition.render(entity, generation:) Success() end diff --git a/app/services/entities/types.rb b/app/services/entities/types.rb index 9a618fd5..510aea32 100644 --- a/app/services/entities/types.rb +++ b/app/services/entities/types.rb @@ -3,9 +3,7 @@ module Entities # Types related to managing and synchronizing an {Entity}. module Types - include Dry.Types - - extend Shared::EnhancedTypes + extend ::Support::Typespace # A pattern for matching an auth_path, composed of multiple slugs AUTH_PATH_FORMAT = /\A[a-z0-9]+(?:(?:\.[a-z0-9]+)|(?(type) do return false unless type.kind_of?(::Dry::Types::Type) diff --git a/app/services/filtering/inputs/comparator_match.rb b/app/services/filtering/inputs/comparator_match.rb index 5e526b25..2221082e 100644 --- a/app/services/filtering/inputs/comparator_match.rb +++ b/app/services/filtering/inputs/comparator_match.rb @@ -3,7 +3,7 @@ module Filtering module Inputs # @abstract - class ComparatorMatch < Shared::FlexibleStruct + class ComparatorMatch < ::Support::FlexibleStruct include Dry::Core::Memoizable BASE_TYPES = Support::DryGQL::TypeContainer.new diff --git a/app/services/filtering/types.rb b/app/services/filtering/types.rb index 15e2feba..5a565b23 100644 --- a/app/services/filtering/types.rb +++ b/app/services/filtering/types.rb @@ -2,9 +2,7 @@ module Filtering module Types - include Dry.Types - - extend Shared::EnhancedTypes + extend ::Support::Typespace Filters = Instance(Filtering::FilterScope) diff --git a/app/services/frontend/types.rb b/app/services/frontend/types.rb index 6ceb3568..06d4d7c3 100644 --- a/app/services/frontend/types.rb +++ b/app/services/frontend/types.rb @@ -2,9 +2,7 @@ module Frontend module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Entity = Instance(HierarchicalEntity) diff --git a/app/services/full_text/types.rb b/app/services/full_text/types.rb index 9df1af07..da68c11d 100644 --- a/app/services/full_text/types.rb +++ b/app/services/full_text/types.rb @@ -6,7 +6,7 @@ module FullText # @see SchematicText # @see Schemas::Properties::Scalar::FullText module Types - include Dry.Types + extend ::Support::Typespace Kind = Coercible::String.enum("text", "markdown", "html").fallback("text").constructor do |value| value.to_s.underscore diff --git a/app/services/geocoding/result.rb b/app/services/geocoding/result.rb index d1480c49..f9ae972a 100644 --- a/app/services/geocoding/result.rb +++ b/app/services/geocoding/result.rb @@ -2,7 +2,7 @@ module Geocoding # @see Geocoding::Lookup - class Result < Shared::FlexibleStruct + class Result < ::Support::FlexibleStruct attribute :country, Geocoding::Types::String attribute? :country_code, Geocoding::Types::String.optional attribute? :region, Geocoding::Types::String.optional diff --git a/app/services/geocoding/types.rb b/app/services/geocoding/types.rb index 226355d4..cfb629a5 100644 --- a/app/services/geocoding/types.rb +++ b/app/services/geocoding/types.rb @@ -2,6 +2,6 @@ module Geocoding module Types - include Dry.Types + extend ::Support::Typespace end end diff --git a/app/services/harvesting/extraction/types.rb b/app/services/harvesting/extraction/types.rb index 496c6985..5f6ce408 100644 --- a/app/services/harvesting/extraction/types.rb +++ b/app/services/harvesting/extraction/types.rb @@ -3,9 +3,7 @@ module Harvesting module Extraction module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Assigns = Coercible::Hash.map(Coercible::String, Any) diff --git a/app/services/harvesting/frozen/types.rb b/app/services/harvesting/frozen/types.rb index 5b776cab..9b65c168 100644 --- a/app/services/harvesting/frozen/types.rb +++ b/app/services/harvesting/frozen/types.rb @@ -4,9 +4,7 @@ module Harvesting module Frozen # Types for top-level harvesting frozen records.. module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Identifier = Coercible::String.constrained(filled: true) diff --git a/app/services/harvesting/metadata/types.rb b/app/services/harvesting/metadata/types.rb index 94e41af1..1b36cfac 100644 --- a/app/services/harvesting/metadata/types.rb +++ b/app/services/harvesting/metadata/types.rb @@ -4,9 +4,7 @@ module Harvesting module Metadata module Types include Dry::Core::Constants - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace ContributionProxy = ::Harvesting::Contributions::Proxy::Type diff --git a/app/services/harvesting/metadata_mappings/types.rb b/app/services/harvesting/metadata_mappings/types.rb index aadede40..bf6c6118 100644 --- a/app/services/harvesting/metadata_mappings/types.rb +++ b/app/services/harvesting/metadata_mappings/types.rb @@ -3,9 +3,7 @@ module Harvesting module MetadataMappings module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Field = ::Types::HarvestMetadataMappingFieldType.dry_type diff --git a/app/services/harvesting/records/skipped.rb b/app/services/harvesting/records/skipped.rb index 0763e1cf..3162ba99 100644 --- a/app/services/harvesting/records/skipped.rb +++ b/app/services/harvesting/records/skipped.rb @@ -5,7 +5,7 @@ module Records # Stores the reason a record was skipped (if any). class Skipped include StoreModel::Model - include Shared::Typing + include ::Support::Typing attribute :active, :boolean, default: false attribute :reason, :string diff --git a/app/services/harvesting/schedules/types.rb b/app/services/harvesting/schedules/types.rb index 5f1c28f9..0e97595e 100644 --- a/app/services/harvesting/schedules/types.rb +++ b/app/services/harvesting/schedules/types.rb @@ -3,7 +3,7 @@ module Harvesting module Schedules module Types - include Dry.Types + extend ::Support::Typespace include Support::EnhancedTypes diff --git a/app/services/harvesting/testing/types.rb b/app/services/harvesting/testing/types.rb index 7a3fb8ed..b5d9f490 100644 --- a/app/services/harvesting/testing/types.rb +++ b/app/services/harvesting/testing/types.rb @@ -4,9 +4,7 @@ module Harvesting module Testing # Types for testing harvesting infrastructure. module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace EsploroRecord = Instance(::EsploroSchema::Record) diff --git a/app/services/harvesting/types.rb b/app/services/harvesting/types.rb index 23402d2a..4bd81a7e 100644 --- a/app/services/harvesting/types.rb +++ b/app/services/harvesting/types.rb @@ -2,9 +2,7 @@ module Harvesting module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace VALID_NAME = /\A([a-z_][a-zA-Z_0-9]*)\z/ diff --git a/app/services/image_attachments/image_wrapper.rb b/app/services/image_attachments/image_wrapper.rb index 787a9bc4..ba41ac77 100644 --- a/app/services/image_attachments/image_wrapper.rb +++ b/app/services/image_attachments/image_wrapper.rb @@ -7,7 +7,7 @@ module ImageAttachments # @see ImageUploader # @see Types::ImageAttachmentType class ImageWrapper - include Shared::Typing + include ::Support::Typing include Dry::Core::Memoizable include Dry::Initializer[undefined: false].define -> do param :attacher, ImageAttachments::Types::Attacher diff --git a/app/services/image_attachments/site_logo_wrapper.rb b/app/services/image_attachments/site_logo_wrapper.rb index f403a465..f1e08158 100644 --- a/app/services/image_attachments/site_logo_wrapper.rb +++ b/app/services/image_attachments/site_logo_wrapper.rb @@ -13,7 +13,7 @@ class SiteLogoWrapper option :purpose, ImageAttachments::Types::Purpose, default: proc { "site_logo" } end - include Shared::Typing + include ::Support::Typing delegate :file, to: :attacher, prefix: :original delegate :alt, :original_filename, to: :original_file, allow_nil: true diff --git a/app/services/image_attachments/size_wrapper.rb b/app/services/image_attachments/size_wrapper.rb index 5c4ca633..b87530e0 100644 --- a/app/services/image_attachments/size_wrapper.rb +++ b/app/services/image_attachments/size_wrapper.rb @@ -6,7 +6,7 @@ module ImageAttachments # # @see Types::ImageSizeType class SizeWrapper - include Shared::Typing + include ::Support::Typing include Dry::Initializer[undefined: false].define -> do param :image_wrapper, AnyImageWrapper param :size, ImageAttachments::Size diff --git a/app/services/image_attachments/types.rb b/app/services/image_attachments/types.rb index d5a2706c..01f807ef 100644 --- a/app/services/image_attachments/types.rb +++ b/app/services/image_attachments/types.rb @@ -3,9 +3,7 @@ module ImageAttachments # Types related to working with image attachments of varying domains. module Types - include Dry.Types - - extend Shared::EnhancedTypes + extend ::Support::Typespace extend Constants diff --git a/app/services/items/types.rb b/app/services/items/types.rb index ebfc7d8d..bff0e572 100644 --- a/app/services/items/types.rb +++ b/app/services/items/types.rb @@ -2,9 +2,7 @@ module Items module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Item = ModelInstance("Item") end diff --git a/app/services/journal_sources/types.rb b/app/services/journal_sources/types.rb index ab3f6585..daa8ee3b 100644 --- a/app/services/journal_sources/types.rb +++ b/app/services/journal_sources/types.rb @@ -2,9 +2,7 @@ module JournalSources module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace UNKNOWN = "UNKNOWN" diff --git a/app/services/keycloak_api/types.rb b/app/services/keycloak_api/types.rb index 78d48af1..a2a5980e 100644 --- a/app/services/keycloak_api/types.rb +++ b/app/services/keycloak_api/types.rb @@ -2,9 +2,7 @@ module KeycloakAPI module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace ClientID = String diff --git a/app/services/layout_invalidations/types.rb b/app/services/layout_invalidations/types.rb index 897f22d3..4cd047f0 100644 --- a/app/services/layout_invalidations/types.rb +++ b/app/services/layout_invalidations/types.rb @@ -2,9 +2,7 @@ module LayoutInvalidations module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace LayoutInvalidation = ModelInstance("LayoutInvalidation") end diff --git a/app/services/layouts/types.rb b/app/services/layouts/types.rb index be057f50..a56159a3 100644 --- a/app/services/layouts/types.rb +++ b/app/services/layouts/types.rb @@ -2,12 +2,10 @@ module Layouts module Types - include Dry.Types + extend ::Support::Typespace include Dry::Core::Constants - extend Support::EnhancedTypes - Association = Coercible::Symbol Associations = Array.of(Association) diff --git a/app/services/links/types.rb b/app/services/links/types.rb index 8b32954e..6fdf00db 100644 --- a/app/services/links/types.rb +++ b/app/services/links/types.rb @@ -3,7 +3,7 @@ module Links # Types related to an {EntityLink}. module Types - include Dry.Types + extend ::Support::Typespace SOURCE_TYPES = %w[Community Collection Item].freeze TARGET_TYPES = SOURCE_TYPES.dup.freeze diff --git a/app/services/liquid_ext/types.rb b/app/services/liquid_ext/types.rb index c4fb7a07..4b3f3920 100644 --- a/app/services/liquid_ext/types.rb +++ b/app/services/liquid_ext/types.rb @@ -2,9 +2,7 @@ module LiquidExt module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace ArgName = Coercible::Symbol.constrained(filled: true) diff --git a/app/services/mappers/types.rb b/app/services/mappers/types.rb index 231b4bd6..8f825ddc 100644 --- a/app/services/mappers/types.rb +++ b/app/services/mappers/types.rb @@ -2,7 +2,7 @@ module Mappers module Types - include Dry.Types + extend ::Support::Typespace include Support::EnhancedTypes diff --git a/app/services/meru/types.rb b/app/services/meru/types.rb index bb2a5086..f88e6711 100644 --- a/app/services/meru/types.rb +++ b/app/services/meru/types.rb @@ -2,9 +2,7 @@ module Meru module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace ClientLocation = ::Types::ClientLocationType.dry_type diff --git a/app/services/metadata/types.rb b/app/services/metadata/types.rb index d7f16bf3..88b98eed 100644 --- a/app/services/metadata/types.rb +++ b/app/services/metadata/types.rb @@ -2,9 +2,7 @@ module Metadata module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace EnumeratedStringList = Coercible::Array.of(Coercible::String.constrained(filled: true)) diff --git a/app/services/mutation_operations/authorization.rb b/app/services/mutation_operations/authorization.rb index bf872df6..ceda680b 100644 --- a/app/services/mutation_operations/authorization.rb +++ b/app/services/mutation_operations/authorization.rb @@ -33,7 +33,7 @@ module ClassMethods # @param [Symbol] arg_key the name of the argument holding the record to authorize # @param [Symbol] with the verb to authorize with, must end with a `"?"` (@see MutationOperations::Types::AuthPredicate) # @return [void] - def authorizes!(arg_key, with:) + def authorizes!(arg_key, with:, each: false) key = MutationOperations::Types::ArgKey[arg_key] predicate = MutationOperations::Types::AuthPredicate[with] @@ -46,6 +46,18 @@ def authorizes!(arg_key, with:) authorize current_user, #{predicate.inspect} end RUBY + return + elsif each + class_eval <<~RUBY, __FILE__, __LINE__ + 1 + before_authorization def authorize_each_in_#{arg_key}_to_#{verb}! + models = Array(args.fetch(#{key.inspect})) + + models.each do |model| + authorize model, #{predicate.inspect} + end + end + RUBY + return end diff --git a/app/services/mutation_operations/base.rb b/app/services/mutation_operations/base.rb index b4fd22f1..e9b276c0 100644 --- a/app/services/mutation_operations/base.rb +++ b/app/services/mutation_operations/base.rb @@ -431,43 +431,6 @@ def set_up_execution_args! end end - # @!endgroup - - module ClassMethods - # Declare that the mutation requires the permission described by `with` - # on a model instance stored in {#args} with the key `arg_key`. - # - # This can be called multiple times if a mutation requires multiple auth checks. - # - # @example Require update permissions for an account - # authorizes! :account, with: :update? - # @param [Symbol] arg_key - # @param [Symbol] with the verb to authorize with - # @return [void] - def authorizes!(arg_key, with:) - key = MutationOperations::Types::ArgKey[arg_key] - - predicate = MutationOperations::Types::AuthPredicate[with] - - verb = predicate.to_s.chomp(??) - - if arg_key == :current_user - class_eval <<~RUBY, __FILE__, __LINE__ + 1 - before_authorization def authorize_current_user_to_#{verb}! - authorize current_user, #{predicate.inspect} - end - RUBY - return - end - - class_eval <<~RUBY, __FILE__, __LINE__ + 1 - before_authorization def authorize_#{arg_key}_to_#{verb}! - model = args.fetch(#{key.inspect}) - - authorize model, #{predicate.inspect} - end - RUBY - end - end + # @!endgroup Mutation State end end diff --git a/app/services/mutation_operations/types.rb b/app/services/mutation_operations/types.rb index 6ea3b2f8..96f5cc28 100644 --- a/app/services/mutation_operations/types.rb +++ b/app/services/mutation_operations/types.rb @@ -3,7 +3,7 @@ module MutationOperations # Types for working with mutation operations, contracts, etc. module Types - include Dry.Types + extend ::Support::Typespace ArgKey = Coercible::Symbol.constrained(format: /\A[a-z][a-z0-9_]+\z/) diff --git a/app/services/mutation_operations/user_error.rb b/app/services/mutation_operations/user_error.rb index 417e8ee7..cf2ebdf4 100644 --- a/app/services/mutation_operations/user_error.rb +++ b/app/services/mutation_operations/user_error.rb @@ -2,7 +2,7 @@ module MutationOperations # @api private - class UserError < Shared::FlexibleStruct + class UserError < ::Support::FlexibleStruct attribute :message, ::Support::GlobalTypes::String attribute :code, ::Support::GlobalTypes::Coercible::String.optional attribute :path, ::Support::DryMutations::Types::AttributePath diff --git a/app/services/named_variable_dates/types.rb b/app/services/named_variable_dates/types.rb index 7f24e027..8648fe86 100644 --- a/app/services/named_variable_dates/types.rb +++ b/app/services/named_variable_dates/types.rb @@ -2,7 +2,7 @@ module NamedVariableDates module Types - include Dry.Types + extend ::Support::Typespace # These variable precision dates are shared across all {ChildEntity} types. SHARED = %i[published].freeze diff --git a/app/services/ordering_invalidations/types.rb b/app/services/ordering_invalidations/types.rb index 2bcaf882..08431b77 100644 --- a/app/services/ordering_invalidations/types.rb +++ b/app/services/ordering_invalidations/types.rb @@ -2,9 +2,7 @@ module OrderingInvalidations module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace OrderingInvalidation = ModelInstance("OrderingInvalidation") end diff --git a/app/services/permalinks/types.rb b/app/services/permalinks/types.rb index 7920cf06..2889369f 100644 --- a/app/services/permalinks/types.rb +++ b/app/services/permalinks/types.rb @@ -2,9 +2,7 @@ module Permalinks module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Permalink = ModelInstance("Permalink") diff --git a/app/services/pilot_harvesting/types.rb b/app/services/pilot_harvesting/types.rb index 5dfcc08e..ddd50a02 100644 --- a/app/services/pilot_harvesting/types.rb +++ b/app/services/pilot_harvesting/types.rb @@ -3,7 +3,7 @@ module PilotHarvesting # Types related to pilot module Types - include Dry.Types + extend ::Support::Typespace SeedList = Types::Array.of(Types::String).default { [] } diff --git a/app/services/protocols/types.rb b/app/services/protocols/types.rb index edbccdc6..af7bbaa2 100644 --- a/app/services/protocols/types.rb +++ b/app/services/protocols/types.rb @@ -2,9 +2,7 @@ module Protocols module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace CurrentPage = Coercible::Integer.constrained(gt: 0).default(1).fallback(1) diff --git a/app/services/rendering/types.rb b/app/services/rendering/types.rb index 9d5d2606..c629e328 100644 --- a/app/services/rendering/types.rb +++ b/app/services/rendering/types.rb @@ -3,9 +3,7 @@ module Rendering # Types related to rendering {Layouts} / {Templates} for {Entities}. module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Generation = String.constrained(uuid_v4: true) end diff --git a/app/services/resolvers/depositor_agreement_transition_resolver.rb b/app/services/resolvers/depositor_agreement_transition_resolver.rb new file mode 100644 index 00000000..e1c61a04 --- /dev/null +++ b/app/services/resolvers/depositor_agreement_transition_resolver.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Resolvers + # A resolver for a {DepositorAgreementTransition}. + # + # @see DepositorAgreementTransition + # @see Types::DepositorAgreementTransitionType + class DepositorAgreementTransitionResolver < AbstractResolver + include Resolvers::Enhancements::PageBasedPagination + + applies_policy_scope! + + type ::Types::DepositorAgreementTransitionType.connection_type, null: false + + resolves_model! ::DepositorAgreementTransition do + object.depositor_agreement_transitions.in_graphql_order + end + end +end diff --git a/app/services/resolvers/submission_batch_publication_transition_resolver.rb b/app/services/resolvers/submission_batch_publication_transition_resolver.rb new file mode 100644 index 00000000..c4499b19 --- /dev/null +++ b/app/services/resolvers/submission_batch_publication_transition_resolver.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Resolvers + # A resolver for a {SubmissionBatchPublicationTransition}. + # + # @see SubmissionBatchPublicationTransition + # @see Types::SubmissionBatchPublicationTransitionType + class SubmissionBatchPublicationTransitionResolver < AbstractResolver + include Resolvers::Enhancements::PageBasedPagination + + applies_policy_scope! + + type ::Types::SubmissionBatchPublicationTransitionType.connection_type, null: false + + resolves_model! ::SubmissionBatchPublicationTransition do + object.submission_batch_publication_transitions.in_graphql_order + end + end +end diff --git a/app/services/resolvers/submission_publication_transition_resolver.rb b/app/services/resolvers/submission_publication_transition_resolver.rb new file mode 100644 index 00000000..ce5fab60 --- /dev/null +++ b/app/services/resolvers/submission_publication_transition_resolver.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Resolvers + # A resolver for a {SubmissionPublicationTransition}. + # + # @see SubmissionPublicationTransition + # @see Types::SubmissionPublicationTransitionType + class SubmissionPublicationTransitionResolver < AbstractResolver + include Resolvers::Enhancements::PageBasedPagination + + applies_policy_scope! + + type ::Types::SubmissionPublicationTransitionType.connection_type, null: false + + resolves_model! ::SubmissionPublicationTransition do + object.submission_publication_transitions.in_graphql_order + end + end +end diff --git a/app/services/resolvers/types.rb b/app/services/resolvers/types.rb index d643bb34..c1818f37 100644 --- a/app/services/resolvers/types.rb +++ b/app/services/resolvers/types.rb @@ -3,9 +3,7 @@ module Resolvers # Types in support of the {Resolvers} subsystem. module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace # A filter scope for use in GraphQL resolvers. # diff --git a/app/services/roles/types.rb b/app/services/roles/types.rb index 35f83ffa..bf70537b 100644 --- a/app/services/roles/types.rb +++ b/app/services/roles/types.rb @@ -2,9 +2,7 @@ module Roles module Types - include Dry.Types - - extend Shared::EnhancedTypes + extend ::Support::Typespace PERMISSION_PART = /(?:[a-z]+?[a-z_]+[a-z]+)/ diff --git a/app/services/schemas/associations/types.rb b/app/services/schemas/associations/types.rb index 735ce6b5..f31677eb 100644 --- a/app/services/schemas/associations/types.rb +++ b/app/services/schemas/associations/types.rb @@ -6,9 +6,7 @@ module Associations # # @api private module Types - include Dry.Types - - extend Shared::EnhancedTypes + extend ::Support::Typespace # An individual {Schemas::Associations::Association association}. Association = Instance(Schemas::Associations::Association) diff --git a/app/services/schemas/orderings/types.rb b/app/services/schemas/orderings/types.rb index 6be20aa0..a55733f5 100644 --- a/app/services/schemas/orderings/types.rb +++ b/app/services/schemas/orderings/types.rb @@ -3,9 +3,7 @@ module Schemas module Orderings module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace COMPONENT_FORMAT = /[a-z][a-z0-9_]*?[a-z0-9]/ diff --git a/app/services/schemas/properties/references/entities/types.rb b/app/services/schemas/properties/references/entities/types.rb index 3b9124d0..670906cd 100644 --- a/app/services/schemas/properties/references/entities/types.rb +++ b/app/services/schemas/properties/references/entities/types.rb @@ -5,7 +5,7 @@ module Properties module References module Entities module Types - include Dry.Types + extend ::Support::Typespace # Parse the name for an ancestor origin. ANCESTOR = /\Aancestor\.(?[a-z][a-z0-9_]*[a-z])\z/ diff --git a/app/services/schemas/properties/type_mapping.rb b/app/services/schemas/properties/type_mapping.rb index fbe360aa..6e90b7e9 100644 --- a/app/services/schemas/properties/type_mapping.rb +++ b/app/services/schemas/properties/type_mapping.rb @@ -3,7 +3,7 @@ module Schemas module Properties class TypeMapping - include Shared::Typing + include ::Support::Typing include Dry::Core::Equalizer.new(:paths) include Dry::Initializer[undefined: false].define -> do diff --git a/app/services/schemas/properties/types.rb b/app/services/schemas/properties/types.rb index 94373d84..67797f99 100644 --- a/app/services/schemas/properties/types.rb +++ b/app/services/schemas/properties/types.rb @@ -4,7 +4,7 @@ module Schemas module Properties # Types specific to working with schema property classes. module Types - include Dry.Types + extend ::Support::Typespace # @api private # diff --git a/app/services/schemas/references/types.rb b/app/services/schemas/references/types.rb index a27715a7..af0f32e3 100644 --- a/app/services/schemas/references/types.rb +++ b/app/services/schemas/references/types.rb @@ -4,9 +4,7 @@ module Schemas module References # Types tied to referencing models from within a schema. module Types - include Dry.Types - - extend Shared::EnhancedTypes + extend ::Support::Typespace Collected = Any.constrained(schematic_collected_references: true).constructor ->(input, &block) do MeruAPI::Container["schemas.references.parse_collected"].call(input).value_or do diff --git a/app/services/schemas/static/types.rb b/app/services/schemas/static/types.rb index bb88a206..484f7d94 100644 --- a/app/services/schemas/static/types.rb +++ b/app/services/schemas/static/types.rb @@ -3,9 +3,7 @@ module Schemas module Static module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Component = ::Schemas::Types::Component diff --git a/app/services/schemas/system/types.rb b/app/services/schemas/system/types.rb index e7c20811..7a1a14bd 100644 --- a/app/services/schemas/system/types.rb +++ b/app/services/schemas/system/types.rb @@ -3,9 +3,7 @@ module Schemas module System module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace # A logical grouping for the kind of property. # diff --git a/app/services/schemas/types.rb b/app/services/schemas/types.rb index d16222fa..967d7161 100644 --- a/app/services/schemas/types.rb +++ b/app/services/schemas/types.rb @@ -3,9 +3,7 @@ module Schemas # Types that are shared across the entire schema ecosystem. module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace # The supported format. Alphanumeric with underscores, all lowercase. # diff --git a/app/services/searching/operator.rb b/app/services/searching/operator.rb index 6b72bb45..8c4566ae 100644 --- a/app/services/searching/operator.rb +++ b/app/services/searching/operator.rb @@ -17,7 +17,7 @@ class Operator include ArelHelpers - include Shared::Typing + include ::Support::Typing define_model_callbacks :prepare, :compile diff --git a/app/services/searching/types.rb b/app/services/searching/types.rb index 567087ad..f91683f8 100644 --- a/app/services/searching/types.rb +++ b/app/services/searching/types.rb @@ -3,9 +3,7 @@ module Searching # Types module for searching subsystem. module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace # @api private OPERATOR_NAMES = %w[ diff --git a/app/services/seeding/brokers/schema_broker.rb b/app/services/seeding/brokers/schema_broker.rb index 0f1ba93d..f39097f3 100644 --- a/app/services/seeding/brokers/schema_broker.rb +++ b/app/services/seeding/brokers/schema_broker.rb @@ -15,7 +15,7 @@ class SchemaBroker option :identifier, Seeding::Types::SchemaIdentifier, default: proc { declaration } end - include Shared::Typing + include ::Support::Typing map_type! key: Seeding::Types::SchemaDeclaration diff --git a/app/services/seeding/import/structs/base.rb b/app/services/seeding/import/structs/base.rb index 2dcf4e3e..37956078 100644 --- a/app/services/seeding/import/structs/base.rb +++ b/app/services/seeding/import/structs/base.rb @@ -4,7 +4,7 @@ module Seeding module Import module Structs # @abstract - class Base < Shared::FlexibleStruct + class Base < ::Support::FlexibleStruct def slice(*keys) keys.flatten.index_with do |key| public_send key diff --git a/app/services/seeding/import/structs/community.rb b/app/services/seeding/import/structs/community.rb index 29fb6855..c761ab00 100644 --- a/app/services/seeding/import/structs/community.rb +++ b/app/services/seeding/import/structs/community.rb @@ -5,7 +5,7 @@ module Import module Structs # A struct for building a {::Community}. class Community < Base - include Shared::Typing + include ::Support::Typing attribute :identifier, Seeding::Types::String attribute :title, Seeding::Types::String diff --git a/app/services/seeding/import/structs/import.rb b/app/services/seeding/import/structs/import.rb index 41e808c3..964e836d 100644 --- a/app/services/seeding/import/structs/import.rb +++ b/app/services/seeding/import/structs/import.rb @@ -5,7 +5,7 @@ module Import module Structs # The root struct for handling an import. class Import < Base - include Shared::Typing + include ::Support::Typing attribute :communities, Seeding::Import::Structs::Community.as_list attribute :version, Seeding::Types::ImportVersion diff --git a/app/services/seeding/import/structs/page.rb b/app/services/seeding/import/structs/page.rb index b27b4b03..6327601e 100644 --- a/app/services/seeding/import/structs/page.rb +++ b/app/services/seeding/import/structs/page.rb @@ -5,7 +5,7 @@ module Import module Structs # A struct for building a {::Page}. class Page < Base - include Shared::Typing + include ::Support::Typing DefaultList = List.default { [] } diff --git a/app/services/seeding/import/structs/properties.rb b/app/services/seeding/import/structs/properties.rb index 4e05d29b..0d20a313 100644 --- a/app/services/seeding/import/structs/properties.rb +++ b/app/services/seeding/import/structs/properties.rb @@ -6,7 +6,7 @@ module Structs # An abstract base properties struct. # @abstract class Properties < Base - include Shared::Typing + include ::Support::Typing class << self def with_default diff --git a/app/services/seeding/types.rb b/app/services/seeding/types.rb index 623bbc16..35d34459 100644 --- a/app/services/seeding/types.rb +++ b/app/services/seeding/types.rb @@ -3,9 +3,7 @@ module Seeding # Types for handling seeding entities. module Types - include Dry.Types - - extend Shared::EnhancedTypes + extend ::Support::Typespace Entity = Instance(::Community) | Instance(::Collection) | Instance(::Item) diff --git a/app/services/settings/types.rb b/app/services/settings/types.rb index c99a52cb..58aa74f1 100644 --- a/app/services/settings/types.rb +++ b/app/services/settings/types.rb @@ -2,9 +2,7 @@ module Settings module Types - include Dry.Types - - extend Shared::EnhancedTypes + extend ::Support::Typespace COLOR_SCHEMES = %w[cream blue gray].freeze diff --git a/app/services/shared/enhanced_types.rb b/app/services/shared/enhanced_types.rb deleted file mode 100644 index 2a3ad369..00000000 --- a/app/services/shared/enhanced_types.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -module Shared - # Enhance a `Dry.Types` module with additional helpers. - # - # @note It should be `extended` rather than `included`. - module EnhancedTypes - # An interface type that matches anything that responds to `#call`. - Operation = Dry::Types["any"].constrained(respond_to: :call) - - class << self - # @return [void] - # @!parse [ruby] - # # An interface type that matches anything that responds to `#call`. - # Operation = Dry::Types["any"].constrained(respond_to: :call) - def extended(mod) - mod.const_set(:Operation, Shared::EnhancedTypes::Operation) - end - end - - # Create a type that matches an exact class or one of its subclasses. - # - # @param [Class] kls - # @return [Dry::Types::Sum::Constrained] - def ClassOrSubclass(kls) - is_klass = ::Dry::Types["class"].constrained(eql: kls) - - is_klass | Implements(kls) - end - - # Create a type that matches a class that includes a given module - # @param [Class, Module] mod - # @return [Dry::Types::Type] - def Implements(mod) - ::Dry::Types["class"].constrained(lt: mod) - end - - alias Inherits Implements - - # A type that can match a class, one of its subclasses, or an instance - # of any of the former. - # - # @param [Class, Module] kls - # @return [Dry::Types::Sum::Constrained] - def InstanceOrClass(mod) - instance = ::Dry::Types::Nominal.new(mod).constrained(type: mod) - klass = ClassOrSubclass(mod) - - instance | klass - end - - # @param [] values - # @param [Object, nil] fallback (will default to first value) - # @param ["coercible.string", "coercible.symbol"] type - # @return [Dry::Types::Type] - def enum_with_fallback(*values, type: "coercible.string", fallback: nil) - values.flatten! - - fallback = fallback.presence_in(values) || values.first - - Dry::Types[type].default(fallback).enum(*values).fallback(fallback) - end - - # The string version of {#enum_with_fallback}. - # - # @param [] values - # @param [String, nil] fallback (will default to first value) - # @return [Dry::Types::Type] - def string_enum(*values, fallback: nil) - enum_with_fallback(*values, type: "coercible.string", fallback:) - end - - # The symbol version of {#enum_with_fallback}. - # - # @param [] values - # @param [Symbol, nil] fallback (will default to first value) - # @return [Dry::Types::Type] - def symbol_enum(*values, fallback: nil) - enum_with_fallback(*values, type: "coercible.symbol", fallback:) - end - end -end diff --git a/app/services/shared/flexible_struct.rb b/app/services/shared/flexible_struct.rb deleted file mode 100644 index 4bf5ace5..00000000 --- a/app/services/shared/flexible_struct.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Shared - # A relaxed dry-struct that accepts stringified keys, - # and will apply defaults when explicitly passed `nil` - # on keys where applicable. - # - # @abstract - class FlexibleStruct < Dry::Struct - transform_keys(&:to_sym) - - transform_types do |type| - if type.default? - type.constructor do |value| - value.nil? ? Dry::Types::Undefined : value - end - else - type - end - end - end -end diff --git a/app/services/shared/type_registry.rb b/app/services/shared/type_registry.rb index 5019e0a9..b38bcf8a 100644 --- a/app/services/shared/type_registry.rb +++ b/app/services/shared/type_registry.rb @@ -20,6 +20,7 @@ module Shared tc.add_model! "ControlledVocabulary" tc.add_model! "ControlledVocabularyItem" tc.add_model! "ControlledVocabularySource" + tc.add_model! "DepositorAgreement" tc.add_model! "DepositorRequest" tc.add_model! "HarvestAttempt" tc.add_model! "HarvestEntity" diff --git a/app/services/shared/typing.rb b/app/services/shared/typing.rb deleted file mode 100644 index 0475ed44..00000000 --- a/app/services/shared/typing.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -module Shared - # Automatically generate `::Type` and `::List` types for any class that includes this. - module Typing - extend ActiveSupport::Concern - - included do - type = Dry::Types::Nominal.new(self).constrained(type: self) - - const_set :Type, type - - list = Dry::Types["array"].of(type) - - const_set :List, list - - subclass = Dry::Types["class"].constrained(lteq: self) - - const_set :Subclass, subclass - end - - class_methods do - def map_type!(key: Dry::Types::Coercible::String, on: :Map) - const_set on, Dry::Types["hash"].map(key, const_get(:Type)) - end - end - - class << self - # Allow our typing module to be applied to modules as well, - # so we can more succinctly type interfaces. - # - # @param [Module] base - # @return [void] - def extend_object(mod) - # :nocov: - raise TypeError, "use #include for classes" if mod.kind_of?(Class) - - # Already extended - return false if mod.singleton_class < self - - # :nocov: - - super - - mod.extend const_get(:ClassMethods) - mod.class_eval(&@_included_block) - end - end - end -end diff --git a/app/services/stale_entities/types.rb b/app/services/stale_entities/types.rb index 341f415d..8a226f3b 100644 --- a/app/services/stale_entities/types.rb +++ b/app/services/stale_entities/types.rb @@ -2,9 +2,7 @@ module StaleEntities module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace StaleEntity = ModelInstance("StaleEntity") end diff --git a/app/services/submission_batch_publications/state_machine.rb b/app/services/submission_batch_publications/state_machine.rb new file mode 100644 index 00000000..d7f4006a --- /dev/null +++ b/app/services/submission_batch_publications/state_machine.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module SubmissionBatchPublications + # @see SubmissionBatchPublication + # @see SubmissionBatchPublicationTransition + # @see Types::SubmissionBatchPublicationStateType + class StateMachine + include Statesman::Machine + + state :pending, initial: true + state :batched + state :finished + + transition from: :pending, to: :batched + transition from: :pending, to: :finished + + transition from: :batched, to: :finished + + after_transition do |submission_batch_publication, transition| + submission_batch_publication.update_column(:state, transition.to_state) + end + end +end diff --git a/app/services/submission_batch_publications/types.rb b/app/services/submission_batch_publications/types.rb new file mode 100644 index 00000000..6162e790 --- /dev/null +++ b/app/services/submission_batch_publications/types.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module SubmissionBatchPublications + # Types for working with {SubmissionBatchPublication} operations and services. + module Types + extend ::Support::Typespace + + Submission = ModelInstance("Submission") + + SubmissionBatchPublication = ModelInstance("SubmissionBatchPublication") + + SubmissionPublication = ModelInstance("SubmissionPublication") + + SubmissionTarget = ModelInstance("SubmissionTarget") + + State = ApplicationRecord.dry_pg_enum("submission_batch_publication_state", default: "pending") + + User = ModelInstance("User") + end +end diff --git a/app/services/submission_publications/publisher.rb b/app/services/submission_publications/publisher.rb new file mode 100644 index 00000000..958654c6 --- /dev/null +++ b/app/services/submission_publications/publisher.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module SubmissionPublications + # This serves as a wrapper around {Submissions::Publisher}. + # + # @see SubmissionPublications::Publish + class Publisher < Support::HookBased::Actor + include Dry::Initializer[undefined: false].define -> do + param :submission_publication, Types::SubmissionPublication + + option :submission, Types::Submission, default: -> { submission_publication.submission } + + option :user, Types::User, default: -> { submission_publication.user } + end + + standard_execution! + + # @return [User, AnonymousUser] + attr_reader :current_user + + # @return [Dry::Monads::Success(SubmissionPublication)] + def call + run_callbacks :execute do + yield prepare! + + yield publish! + end + + Success submission_publication.reload + end + + wrapped_hook! def prepare + @current_user = user || AnonymousUser.new + + super + end + + wrapped_hook! def publish + yield submission.publish(submission_publication:, user:) + + super + end + + around_publish :set_current_user! + + private + + # @return [void] + def set_current_user! + ::Support::Requests::Current.set(current_user:) do + yield + end + end + end +end diff --git a/app/services/submission_publications/state_machine.rb b/app/services/submission_publications/state_machine.rb new file mode 100644 index 00000000..e4fd7d68 --- /dev/null +++ b/app/services/submission_publications/state_machine.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module SubmissionPublications + # @see SubmissionPublication + # @see SubmissionPublicationTransition + # @see Types::SubmissionPublicationStateType + class StateMachine + include Statesman::Machine + + state :pending, initial: true + state :batched + state :success + state :failure + + transition from: :pending, to: :batched + transition from: :pending, to: :failure + transition from: :pending, to: :success + + transition from: :batched, to: %i[success failure] + + transition from: :failure, to: :pending + + after_transition do |submission_publication, transition| + submission_publication.update_column(:state, transition.to_state) + end + end +end diff --git a/app/services/submission_publications/types.rb b/app/services/submission_publications/types.rb new file mode 100644 index 00000000..279020cd --- /dev/null +++ b/app/services/submission_publications/types.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module SubmissionPublications + # Types for working with {SubmissionPublication} operations and services. + module Types + extend ::Support::Typespace + + Submission = ModelInstance("Submission") + + SubmissionBatchPublication = ModelInstance("SubmissionBatchPublication") + + SubmissionPublication = ModelInstance("SubmissionPublication") + + SubmissionTarget = ModelInstance("SubmissionTarget") + + State = ApplicationRecord.dry_pg_enum("submission_publication_state", default: "pending") + + User = ModelInstance("User") + end +end diff --git a/app/services/submission_reviews/state_machine.rb b/app/services/submission_reviews/state_machine.rb index 90e73617..ffcfa59b 100644 --- a/app/services/submission_reviews/state_machine.rb +++ b/app/services/submission_reviews/state_machine.rb @@ -8,9 +8,14 @@ class StateMachine include Support::StatesmanHelpers::Machine state :pending, initial: true + state :revision_requested state :approved state :rejected flexible_transitions! + + after_transition do |submission_review, transition| + submission_review.update_column(:state, transition.to_state) + end end end diff --git a/app/services/submission_targets/batch_publisher.rb b/app/services/submission_targets/batch_publisher.rb new file mode 100644 index 00000000..c85aa56e --- /dev/null +++ b/app/services/submission_targets/batch_publisher.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module SubmissionTargets + # @see SubmissionTargets::BatchPublish + class BatchPublisher < Support::HookBased::Actor + include Dry::Initializer[undefined: false].define -> do + param :submission_target, Types::SubmissionTarget + + param :submissions, Types::Submissions + + option :user, Types::User.optional, optional: true + end + + standard_execution! + + # @return [] + attr_reader :submission_publications + + # @return [SubmissionBatchPublication] + attr_reader :submission_batch_publication + + # @return [Dry::Monads::Success(SubmissionBatchPublication)] + def call + run_callbacks :execute do + yield prepare! + + yield enqueue! + end + + Success(submission_batch_publication) + end + + wrapped_hook! def prepare + @submission_batch_publication = submission_target.submission_batch_publications.create!(user:) + + @submission_publications = [] + + super + end + + wrapped_hook! def enqueue + GoodJob::Batch.enqueue(on_finish: SubmissionBatchPublications::FinishJob, submission_batch_publication:) do + submissions.each.with_index(1) do |submission, index| + enqueue_publication_for!(submission, index) + end + + submission_batch_publication.transition_to! :batched + end + + super + end + + private + + # @param [Submission] submission + # @param [Integer] batch_position + # @return [void] + def enqueue_publication_for!(submission, batch_position) + publication = submission.submission_publications.create!(user:, submission_batch_publication:, batch_position:) + + SubmissionPublications::PublishJob.perform_later(publication) + + publication.transition_to! :batched + + submission_publications << publication + end + end +end diff --git a/app/services/submission_targets/types.rb b/app/services/submission_targets/types.rb index 1e0e481d..8d855c0a 100644 --- a/app/services/submission_targets/types.rb +++ b/app/services/submission_targets/types.rb @@ -3,9 +3,7 @@ module SubmissionTargets # Types for working with {SubmissionTarget} operations and services. module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace DepositMode = ApplicationRecord.dry_pg_enum(:submission_deposit_mode, default: "direct").fallback("direct") @@ -21,8 +19,12 @@ module Types Submission = ModelInstance("Submission") + Submissions = Array.of(Submission) + SubmissionTarget = ModelInstance("SubmissionTarget") Configurable = Entity | SubmissionTarget + + User = ModelInstance("User") end end diff --git a/app/services/submissions/publisher.rb b/app/services/submissions/publisher.rb index e9f6b2b0..37d254b4 100644 --- a/app/services/submissions/publisher.rb +++ b/app/services/submissions/publisher.rb @@ -6,23 +6,78 @@ module Submissions class Publisher < Support::HookBased::Actor include Dry::Initializer[undefined: false].define -> do param :submission, Submissions::Types::Submission + + option :submission_publication, Submissions::Types::SubmissionPublication.optional, optional: true, as: :provided_publication + + option :user, Submissions::Types::User.optional, optional: true end standard_execution! - # @return [Dry::Monads::Success(Submission)] + delegate :entity, to: :submission + + # @return [SubmissionPublication] + attr_reader :submission_publication + + # @return [Dry::Monads::Success(SubmissionPublication)] def call run_callbacks :execute do yield prepare! + + yield publish_entity! end - Success submission - rescue Statesman::TransitionFailedError - Success submission + Success submission_publication end wrapped_hook! def prepare + @submission_publication = provided_publication || submission.submission_publications.find_or_create_by!(user:) + super end + + wrapped_hook! def try_to_publish + actually_publish_entity! + + handle_state_transitions! + + super + end + + around_try_to_publish :wrap_in_transaction! + + wrapped_hook! def publish_entity + try_to_publish! + + super + rescue StandardError => e + submission_publication.transition_to(:failure, reason: e.message) + + super + end + + private + + # @return [void] + def actually_publish_entity! + entity.published ||= VariablePrecisionDate.parse(Date.current) + + entity.submission_status = "submission_published" + + entity.save! + end + + # @return [void] + def handle_state_transitions! + submission_publication.transition_to!(:success) + + submission.transition_to!(:published) + end + + def wrap_in_transaction! + ActiveRecord::Base.transaction do + yield + end + end end end diff --git a/app/services/submissions/status.rb b/app/services/submissions/status.rb index 3eff181c..a11e85cc 100644 --- a/app/services/submissions/status.rb +++ b/app/services/submissions/status.rb @@ -28,17 +28,41 @@ class Status published ].freeze - def mutable_state? = to_state.in?(MUTABLE_STATES) + # @return [Boolean] + attr_reader :any_published - alias mutable_state mutable_state? + alias any_published? any_published - def locked_state? = to_state.in?(LOCKED_STATES) + # @return [Boolean] + attr_reader :current - alias locked_state locked_state? + alias current? current + + # @return [Boolean] + attr_reader :locked_state + + alias locked_state? locked_state + + # @return [Boolean] + attr_reader :mutable_state + + alias mutable_state? mutable_state + + def initialize(...) + super + + @any_published = from_state == "published" || to_state == "published" + + @current = from_state == to_state + + @locked_state = to_state.in?(LOCKED_STATES) + + @mutable_state = to_state.in?(MUTABLE_STATES) + end class << self # @return [Class] - def policy_class = SubmissionStatusPolicy + def policy_class = Submissions::StatusPolicy end # @return [Class] diff --git a/app/services/submissions/types.rb b/app/services/submissions/types.rb index 7941916b..91e71738 100644 --- a/app/services/submissions/types.rb +++ b/app/services/submissions/types.rb @@ -3,14 +3,16 @@ module Submissions # Types for working with {Submission} operations and services. module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Submission = ModelInstance("Submission") + SubmissionPublication = ModelInstance("SubmissionPublication") + SubmissionTarget = ModelInstance("SubmissionTarget") State = ApplicationRecord.dry_pg_enum("submission_state", default: "draft") + + User = ModelInstance("User") end end diff --git a/app/services/templates/config/utility/types.rb b/app/services/templates/config/utility/types.rb index 2a3172c2..b9b15943 100644 --- a/app/services/templates/config/utility/types.rb +++ b/app/services/templates/config/utility/types.rb @@ -5,9 +5,7 @@ module Config module Utility # Very specific utility types for the config namespace within the templating subsystem. module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace LayoutConfigKlass = Inherits(::Templates::Config::Utility::AbstractLayout) diff --git a/app/services/templates/drops/abstract_contributions_drop.rb b/app/services/templates/drops/abstract_contributions_drop.rb index daf71a3b..ccf7179e 100644 --- a/app/services/templates/drops/abstract_contributions_drop.rb +++ b/app/services/templates/drops/abstract_contributions_drop.rb @@ -42,7 +42,7 @@ def to_s # @abstract # @return [ActiveRecord::Relation<::Contribution>] def fetch_contributions - @entity.contributions + @entity.contributions.includes(:contributor) end end end diff --git a/app/services/templates/tags/types.rb b/app/services/templates/tags/types.rb index 141f02d0..16b84251 100644 --- a/app/services/templates/tags/types.rb +++ b/app/services/templates/tags/types.rb @@ -3,9 +3,7 @@ module Templates module Tags module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace ArgName = Coercible::Symbol.constrained(filled: true) diff --git a/app/services/templates/types.rb b/app/services/templates/types.rb index c9f0f640..9849baef 100644 --- a/app/services/templates/types.rb +++ b/app/services/templates/types.rb @@ -2,9 +2,7 @@ module Templates module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Asset = ModelInstance("Asset") diff --git a/app/services/testing/assign_random_roles.rb b/app/services/testing/assign_random_roles.rb deleted file mode 100644 index 3a29d251..00000000 --- a/app/services/testing/assign_random_roles.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module Testing - class AssignRandomRoles - include MeruAPI::Deps[grant_access: "access.grant"] - include Dry::Monads[:do, :result] - prepend HushActiveRecord - - def call - roles = [Role.fetch("manager"), Role.fetch("editor")] - - communities = Community.sample(5) - collections = Collection.where.not(community: communities).sample(50) - items = Item.where.not(collection: collections).sample(100) - - things = [*communities, *collections, *items].shuffle - - User.testing.find_each do |user| - things.sample(20).each do |thing| - role = roles.sample - - yield grant_access.call role, on: thing, to: user - end - end - end - end -end diff --git a/app/services/testing/hush_active_record.rb b/app/services/testing/hush_active_record.rb deleted file mode 100644 index d2baf236..00000000 --- a/app/services/testing/hush_active_record.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Testing - module HushActiveRecord - def call(...) - ActiveRecord::Base.logger.silence do - super - end - end - end -end diff --git a/app/services/testing/mock_controller.rb b/app/services/testing/mock_controller.rb deleted file mode 100644 index 54a70912..00000000 --- a/app/services/testing/mock_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -module Testing - # @api private - class MockController - include Dry::Initializer[undefined: false].define -> do - param :request, Support::GlobalTypes.Instance(ActionDispatch::Request) - end - end -end diff --git a/app/services/testing/random_ip_address_set.rb b/app/services/testing/random_ip_address_set.rb deleted file mode 100644 index a4c8530b..00000000 --- a/app/services/testing/random_ip_address_set.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -module Testing - # A file-backed set of random IP addresses. - class RandomIPAddressSet - include Dry::Core::Memoizable - include Enumerable - - PATH = Rails.root.join("vendor", "random_ip_addresses.yaml") - - delegate :each, :sample, :size, to: :addresses - - def addresses - cache.compute_if_absent __method__ do - YAML.load_file PATH - end - end - - # @param [] addresses - # @return [void] - def write!(addresses) - PATH.open "w+" do |f| - f.write(<<~DISCLAIMER) - # The following IP addresses are randomly generated and used for testing purposes only. - DISCLAIMER - - f.write YAML.dump addresses - end - - reload! - end - - private - - memoize def cache - Concurrent::Map.new - end - - # @return [void] - def reload! - cache.clear - - return self - end - - class << self - def instance - @instance ||= new - end - - delegate_missing_to :instance - end - end -end diff --git a/app/services/testing/tree_ordering_adder.rb b/app/services/testing/tree_ordering_adder.rb deleted file mode 100644 index 1a73c05a..00000000 --- a/app/services/testing/tree_ordering_adder.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -module Testing - # @api private - class TreeOrderingAdder - include Dry::Monads[:do, :result] - include MonadicPersistence - include Dry::Initializer[undefined: false].define -> do - option :id, Dry::Types["coercible.string"], default: proc { "test_tree" } - option :name, Dry::Types["coercible.string"], default: proc { "Test Tree" } - option :position, Dry::Types["coercible.integer"].constrained(gt: 0), default: proc { 10_000 } - end - - # @param [SchemaInstance] entity - # @return [Ordering] - def call(entity) - ordering = entity.orderings.by_identifier(id).first_or_initialize do |ord| - ord.schema_version = entity.schema_version - end - - ordering.definition = build_definition - - ordering.schema_position = position - ordering.position = position - - monadic_save ordering - end - - private - - def build_definition - ::Support::PropertyHash.new.tap do |d| - d["id"] = id - d["name"] = name - d["render.mode"] = "tree" - d["select.direct"] = "descendants" - d["order"] = [ - { "path" => "entity.title", "direction" => "asc" } - ] - d["hidden"] = true - d["position"] = position - end.to_h - end - end -end diff --git a/app/services/testing/types.rb b/app/services/testing/types.rb deleted file mode 100644 index 077e13d1..00000000 --- a/app/services/testing/types.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -module Testing - # @api private - module Types - include Dry.Types - - HTTPMethod = String.enum("HEAD", "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") - end -end diff --git a/app/services/users/types.rb b/app/services/users/types.rb index 545dacd4..79a5cb77 100644 --- a/app/services/users/types.rb +++ b/app/services/users/types.rb @@ -6,7 +6,7 @@ module Users # @see ::AnonymousUser # @see ::User module Types - include Dry.Types + extend ::Support::Typespace AccessManagement = ApplicationRecord.dry_pg_enum("access_management", default: "forbidden").fallback("forbidden") diff --git a/app/services/utility/string_cleaning/substitution.rb b/app/services/utility/string_cleaning/substitution.rb index a44a6905..846368b5 100644 --- a/app/services/utility/string_cleaning/substitution.rb +++ b/app/services/utility/string_cleaning/substitution.rb @@ -6,7 +6,7 @@ module StringCleaning # # A substitution entry for a {Utility::StringCleaner}. class Substitution < Dry::Struct - include Shared::Typing + include ::Support::Typing Pattern = Support::GlobalTypes.Instance(::Regexp) | Dry::Types["string"] diff --git a/app/services/utility/types.rb b/app/services/utility/types.rb index 84981de6..ee0cec89 100644 --- a/app/services/utility/types.rb +++ b/app/services/utility/types.rb @@ -3,9 +3,7 @@ module Utility module Types include Dry::Core::Constants - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace Callable = Interface(:to_proc) diff --git a/bin/derailed b/bin/derailed new file mode 100755 index 00000000..511b716d --- /dev/null +++ b/bin/derailed @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'derailed' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("derailed_benchmarks", "derailed") diff --git a/config/initializers/900_good_job.rb b/config/initializers/900_good_job.rb index 958e3989..5041ff10 100644 --- a/config/initializers/900_good_job.rb +++ b/config/initializers/900_good_job.rb @@ -9,7 +9,7 @@ "rendering:1", "+purging,hierarchies,entities,orderings,invalidations,layouts:2", "+harvest_pruning,extraction,harvesting,asset_fetching:2", - "default,mailers,ahoy,processing,cache_warming:2", + "default,mailers,ahoy,depositing,processing,cache_warming:2", ].join(?;) config.good_job.preserve_job_records = :on_unhandled_error diff --git a/config/locales/en.yml b/config/locales/en.yml index b55a97e3..5c6b60d6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -60,6 +60,7 @@ en: limited_visibility_requires_range: "must have a range set when visibility is limited" linked_to_parent: "cannot be linked to its parent" linked_to_itself: "cannot be linked to itself" + mismatched_batch_submission_target: "is not a part of the provided batch submission target" mismatched_vocabulary_item: "is not a part of the provided vocabulary" must_be_associated_with_entity: "must be associated with the provided entity" must_be_child_entity_schema: "must be a collection or item schema" @@ -70,6 +71,7 @@ en: must_be_new_state: "must not be the current state" must_be_orcid: "must be a valid ORCID URL: https://orcid.org/xxxx-xxxx-xxxx-xxxx" must_be_ordering: "must be an ordering" + must_be_publishable: "must be in a state that can be published" must_be_slug: "must be a valid slug (all lowercase, no whitespace)" must_be_unique: "must be unique" must_be_unique_doi: "must be a unique DOI" diff --git a/db/migrate/20260312162710_adjust_submission_review_state.rb b/db/migrate/20260312162710_adjust_submission_review_state.rb new file mode 100644 index 00000000..60b7e98d --- /dev/null +++ b/db/migrate/20260312162710_adjust_submission_review_state.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AdjustSubmissionReviewState < ActiveRecord::Migration[7.2] + def up + execute <<~SQL + ALTER TYPE submission_review_state ADD VALUE IF NOT EXISTS 'revision_requested' AFTER 'pending'; + SQL + end + + def down + # Intentionally left blank. + end +end diff --git a/db/migrate/20260312195907_create_submission_publications.rb b/db/migrate/20260312195907_create_submission_publications.rb new file mode 100644 index 00000000..ce765c70 --- /dev/null +++ b/db/migrate/20260312195907_create_submission_publications.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class CreateSubmissionPublications < ActiveRecord::Migration[7.2] + def change + create_enum :submission_batch_publication_state, ["pending", "batched", "finished"] + + create_enum :submission_publication_state, ["pending", "batched", "success", "failure"] + + create_table :submission_batch_publications, id: :uuid do |t| + t.references :submission_target, null: false, foreign_key: { on_delete: :cascade }, type: :uuid + t.references :user, null: true, foreign_key: { on_delete: :nullify }, type: :uuid + + t.enum :state, enum_type: "submission_batch_publication_state", null: false, default: "pending" + + t.bigint :publications_count, null: false, default: 0 + + t.timestamps null: false, default: -> { "CURRENT_TIMESTAMP" } + end + + build_transition_table_for :submission_batch_publication + + create_table :submission_publications, id: :uuid do |t| + t.references :submission, null: false, foreign_key: { on_delete: :cascade }, type: :uuid + t.references :user, null: true, foreign_key: { on_delete: :nullify }, type: :uuid + t.references :submission_batch_publication, null: true, foreign_key: { on_delete: :nullify }, type: :uuid + + t.enum :state, enum_type: "submission_publication_state", null: false, default: "pending" + + t.bigint :batch_position, null: true + + t.timestamps null: false, default: -> { "CURRENT_TIMESTAMP" } + end + + build_transition_table_for :submission_publication + end + + private + + def build_transition_table_for(base) + enum_type = :"#{base}_state" + + create_table :"#{base}_transitions", id: :uuid do |t| + t.references base, null: false, type: :uuid, foreign_key: { on_delete: :cascade }, index: false + t.references :user, null: true, type: :uuid, foreign_key: { on_delete: :nullify } + + t.boolean :most_recent, null: false + t.integer :sort_key, null: false + t.enum :from_state, enum_type:, null: true + t.enum :to_state, enum_type:, null: false + t.jsonb :metadata + + t.timestamps null: false, default: -> { "CURRENT_TIMESTAMP" } + + t.index %I(#{base}_id sort_key), unique: true, name: "idx_#{base}_transitions_parent_sort" + t.index %I(#{base}_id most_recent), unique: true, where: "most_recent", name: "idx_#{base}_transitions_parent_most_recent" + end + end +end diff --git a/db/migrate/20260312201100_create_depositor_agreements.rb b/db/migrate/20260312201100_create_depositor_agreements.rb new file mode 100644 index 00000000..8ad0d478 --- /dev/null +++ b/db/migrate/20260312201100_create_depositor_agreements.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class CreateDepositorAgreements < ActiveRecord::Migration[7.2] + def change + create_enum :depositor_agreement_state, %w[pending accepted] + + create_table :depositor_agreements, id: :uuid do |t| + t.references :submission_target, null: false, foreign_key: { on_delete: :cascade }, type: :uuid + + t.references :user, null: false, foreign_key: { on_delete: :cascade }, type: :uuid + + t.enum :state, enum_type: :depositor_agreement_state, null: false, default: "pending" + + t.timestamp :last_accepted_at + + t.timestamps null: false, default: -> { "CURRENT_TIMESTAMP" } + + t.index %i[submission_target_id user_id], unique: true, name: "idx_depositor_agreements_uniqueness" + end + + build_transition_table_for :depositor_agreement + end + + private + + def build_transition_table_for(base) + enum_type = :"#{base}_state" + + create_table :"#{base}_transitions", id: :uuid do |t| + t.references base, null: false, type: :uuid, foreign_key: { on_delete: :cascade }, index: false + t.references :user, null: true, type: :uuid, foreign_key: { on_delete: :nullify } + + t.boolean :most_recent, null: false + t.integer :sort_key, null: false + t.enum :from_state, enum_type:, null: true + t.enum :to_state, enum_type:, null: false + t.jsonb :metadata + + t.timestamps null: false, default: -> { "CURRENT_TIMESTAMP" } + + t.index %I(#{base}_id sort_key), unique: true, name: "idx_#{base}_transitions_parent_sort" + t.index %I(#{base}_id most_recent), unique: true, where: "most_recent", name: "idx_#{base}_transitions_parent_most_recent" + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 3510aa57..30b0b2db 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -266,6 +266,16 @@ CREATE TYPE public.date_precision AS ENUM ( ); +-- +-- Name: depositor_agreement_state; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.depositor_agreement_state AS ENUM ( + 'pending', + 'accepted' +); + + -- -- Name: depositor_request_state; Type: TYPE; Schema: public; Owner: - -- @@ -885,6 +895,17 @@ CREATE TYPE public.sibling_kind AS ENUM ( ); +-- +-- Name: submission_batch_publication_state; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.submission_batch_publication_state AS ENUM ( + 'pending', + 'batched', + 'finished' +); + + -- -- Name: submission_comment_role; Type: TYPE; Schema: public; Owner: - -- @@ -905,12 +926,25 @@ CREATE TYPE public.submission_deposit_mode AS ENUM ( ); +-- +-- Name: submission_publication_state; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.submission_publication_state AS ENUM ( + 'pending', + 'batched', + 'success', + 'failure' +); + + -- -- Name: submission_review_state; Type: TYPE; Schema: public; Owner: - -- CREATE TYPE public.submission_review_state AS ENUM ( 'pending', + 'revision_requested', 'approved', 'rejected' ); @@ -4812,6 +4846,39 @@ CREATE TABLE public.controlled_vocabulary_sources ( ); +-- +-- Name: depositor_agreement_transitions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.depositor_agreement_transitions ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + depositor_agreement_id uuid NOT NULL, + user_id uuid, + most_recent boolean NOT NULL, + sort_key integer NOT NULL, + from_state public.depositor_agreement_state, + to_state public.depositor_agreement_state NOT NULL, + metadata jsonb, + 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: depositor_agreements; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.depositor_agreements ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + submission_target_id uuid NOT NULL, + user_id uuid NOT NULL, + state public.depositor_agreement_state DEFAULT 'pending'::public.depositor_agreement_state NOT NULL, + last_accepted_at timestamp without time zone, + 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: depositor_request_transitions; Type: TABLE; Schema: public; Owner: - -- @@ -7151,6 +7218,39 @@ CREATE VIEW public.stale_entities AS ORDER BY layout_invalidations.entity_id, layout_invalidations.stale_at DESC; +-- +-- Name: submission_batch_publication_transitions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.submission_batch_publication_transitions ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + submission_batch_publication_id uuid NOT NULL, + user_id uuid, + most_recent boolean NOT NULL, + sort_key integer NOT NULL, + from_state public.submission_batch_publication_state, + to_state public.submission_batch_publication_state NOT NULL, + metadata jsonb, + 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: submission_batch_publications; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.submission_batch_publications ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + submission_target_id uuid NOT NULL, + user_id uuid, + state public.submission_batch_publication_state DEFAULT 'pending'::public.submission_batch_publication_state NOT NULL, + publications_count bigint DEFAULT 0 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: submission_comments; Type: TABLE; Schema: public; Owner: - -- @@ -7186,6 +7286,40 @@ CREATE TABLE public.submission_deposit_targets ( ); +-- +-- Name: submission_publication_transitions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.submission_publication_transitions ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + submission_publication_id uuid NOT NULL, + user_id uuid, + most_recent boolean NOT NULL, + sort_key integer NOT NULL, + from_state public.submission_publication_state, + to_state public.submission_publication_state NOT NULL, + metadata jsonb, + 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: submission_publications; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.submission_publications ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + submission_id uuid NOT NULL, + user_id uuid, + submission_batch_publication_id uuid, + state public.submission_publication_state DEFAULT 'pending'::public.submission_publication_state NOT NULL, + batch_position bigint, + 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: submission_review_transitions; Type: TABLE; Schema: public; Owner: - -- @@ -8665,6 +8799,22 @@ ALTER TABLE ONLY public.controlled_vocabulary_sources ADD CONSTRAINT controlled_vocabulary_sources_pkey PRIMARY KEY (id); +-- +-- Name: depositor_agreement_transitions depositor_agreement_transitions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.depositor_agreement_transitions + ADD CONSTRAINT depositor_agreement_transitions_pkey PRIMARY KEY (id); + + +-- +-- Name: depositor_agreements depositor_agreements_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.depositor_agreements + ADD CONSTRAINT depositor_agreements_pkey PRIMARY KEY (id); + + -- -- Name: depositor_request_transitions depositor_request_transitions_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -9409,6 +9559,22 @@ ALTER TABLE ONLY public.schematic_texts ADD CONSTRAINT schematic_texts_pkey PRIMARY KEY (id); +-- +-- Name: submission_batch_publication_transitions submission_batch_publication_transitions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.submission_batch_publication_transitions + ADD CONSTRAINT submission_batch_publication_transitions_pkey PRIMARY KEY (id); + + +-- +-- Name: submission_batch_publications submission_batch_publications_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.submission_batch_publications + ADD CONSTRAINT submission_batch_publications_pkey PRIMARY KEY (id); + + -- -- Name: submission_comments submission_comments_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -9425,6 +9591,22 @@ ALTER TABLE ONLY public.submission_deposit_targets ADD CONSTRAINT submission_deposit_targets_pkey PRIMARY KEY (id); +-- +-- Name: submission_publication_transitions submission_publication_transitions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.submission_publication_transitions + ADD CONSTRAINT submission_publication_transitions_pkey PRIMARY KEY (id); + + +-- +-- Name: submission_publications submission_publications_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.submission_publications + ADD CONSTRAINT submission_publications_pkey PRIMARY KEY (id); + + -- -- Name: submission_review_transitions submission_review_transitions_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -9867,6 +10049,27 @@ CREATE UNIQUE INDEX harvest_entity_anc_desc_idx ON public.harvest_entity_hierarc CREATE INDEX harvest_entity_desc_idx ON public.harvest_entity_hierarchies USING btree (descendant_id); +-- +-- Name: idx_depositor_agreement_transitions_parent_most_recent; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_depositor_agreement_transitions_parent_most_recent ON public.depositor_agreement_transitions USING btree (depositor_agreement_id, most_recent) WHERE most_recent; + + +-- +-- Name: idx_depositor_agreement_transitions_parent_sort; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_depositor_agreement_transitions_parent_sort ON public.depositor_agreement_transitions USING btree (depositor_agreement_id, sort_key); + + +-- +-- Name: idx_depositor_agreements_uniqueness; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_depositor_agreements_uniqueness ON public.depositor_agreements USING btree (submission_target_id, user_id); + + -- -- Name: idx_depositor_request_transitions_parent_most_recent; Type: INDEX; Schema: public; Owner: - -- @@ -9937,6 +10140,13 @@ CREATE INDEX idx_layouts_supplementary_instances_defn ON public.layouts_suppleme CREATE INDEX idx_on_list_item_layout_instance_id_3e916a1bef ON public.templates_cached_entity_list_items USING btree (list_item_layout_instance_id); +-- +-- Name: idx_on_submission_batch_publication_id_c91de0cb2b; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_on_submission_batch_publication_id_c91de0cb2b ON public.submission_publications USING btree (submission_batch_publication_id); + + -- -- Name: idx_on_submission_target_id_665af0c713; Type: INDEX; Schema: public; Owner: - -- @@ -9944,6 +10154,20 @@ CREATE INDEX idx_on_list_item_layout_instance_id_3e916a1bef ON public.templates_ CREATE INDEX idx_on_submission_target_id_665af0c713 ON public.submission_target_schema_versions USING btree (submission_target_id); +-- +-- Name: idx_submission_batch_publication_transitions_parent_most_recent; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_submission_batch_publication_transitions_parent_most_recent ON public.submission_batch_publication_transitions USING btree (submission_batch_publication_id, most_recent) WHERE most_recent; + + +-- +-- Name: idx_submission_batch_publication_transitions_parent_sort; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_submission_batch_publication_transitions_parent_sort ON public.submission_batch_publication_transitions USING btree (submission_batch_publication_id, sort_key); + + -- -- Name: idx_submission_comments_positioning; Type: INDEX; Schema: public; Owner: - -- @@ -9958,6 +10182,20 @@ CREATE UNIQUE INDEX idx_submission_comments_positioning ON public.submission_com CREATE UNIQUE INDEX idx_submission_deposit_targets_uniqueness ON public.submission_deposit_targets USING btree (submission_target_id, entity_type, entity_id); +-- +-- Name: idx_submission_publication_transitions_parent_most_recent; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_submission_publication_transitions_parent_most_recent ON public.submission_publication_transitions USING btree (submission_publication_id, most_recent) WHERE most_recent; + + +-- +-- Name: idx_submission_publication_transitions_parent_sort; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX idx_submission_publication_transitions_parent_sort ON public.submission_publication_transitions USING btree (submission_publication_id, sort_key); + + -- -- Name: idx_submission_review_transitions_parent_most_recent; Type: INDEX; Schema: public; Owner: - -- @@ -11127,6 +11365,27 @@ CREATE INDEX index_controlled_vocabulary_sources_on_controlled_vocabulary_id ON CREATE UNIQUE INDEX index_controlled_vocabulary_sources_on_provides ON public.controlled_vocabulary_sources USING btree (provides); +-- +-- Name: index_depositor_agreement_transitions_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_depositor_agreement_transitions_on_user_id ON public.depositor_agreement_transitions USING btree (user_id); + + +-- +-- Name: index_depositor_agreements_on_submission_target_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_depositor_agreements_on_submission_target_id ON public.depositor_agreements USING btree (submission_target_id); + + +-- +-- Name: index_depositor_agreements_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_depositor_agreements_on_user_id ON public.depositor_agreements USING btree (user_id); + + -- -- Name: index_depositor_request_transitions_on_user_id; Type: INDEX; Schema: public; Owner: - -- @@ -13465,6 +13724,27 @@ CREATE INDEX index_schematic_texts_on_document ON public.schematic_texts USING g CREATE INDEX index_schematic_texts_on_schema_version_property_id ON public.schematic_texts USING btree (schema_version_property_id); +-- +-- Name: index_submission_batch_publication_transitions_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_submission_batch_publication_transitions_on_user_id ON public.submission_batch_publication_transitions USING btree (user_id); + + +-- +-- Name: index_submission_batch_publications_on_submission_target_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_submission_batch_publications_on_submission_target_id ON public.submission_batch_publications USING btree (submission_target_id); + + +-- +-- Name: index_submission_batch_publications_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_submission_batch_publications_on_user_id ON public.submission_batch_publications USING btree (user_id); + + -- -- Name: index_submission_comments_on_role; Type: INDEX; Schema: public; Owner: - -- @@ -13528,6 +13808,27 @@ CREATE INDEX index_submission_deposit_targets_on_schema_version_id ON public.sub CREATE INDEX index_submission_deposit_targets_on_submission_target_id ON public.submission_deposit_targets USING btree (submission_target_id); +-- +-- Name: index_submission_publication_transitions_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_submission_publication_transitions_on_user_id ON public.submission_publication_transitions USING btree (user_id); + + +-- +-- Name: index_submission_publications_on_submission_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_submission_publications_on_submission_id ON public.submission_publications USING btree (submission_id); + + +-- +-- Name: index_submission_publications_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_submission_publications_on_user_id ON public.submission_publications USING btree (user_id); + + -- -- Name: index_submission_review_transitions_on_user_id; Type: INDEX; Schema: public; Owner: - -- @@ -14530,6 +14831,14 @@ ALTER TABLE ONLY public.templates_navigation_instances ADD CONSTRAINT fk_rails_05ddde0077 FOREIGN KEY (layout_instance_id) REFERENCES public.layouts_navigation_instances(id) ON DELETE CASCADE; +-- +-- Name: depositor_agreement_transitions fk_rails_062db08035; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.depositor_agreement_transitions + ADD CONSTRAINT fk_rails_062db08035 FOREIGN KEY (depositor_agreement_id) REFERENCES public.depositor_agreements(id) ON DELETE CASCADE; + + -- -- Name: harvest_configurations fk_rails_077935b75c; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -14594,6 +14903,14 @@ ALTER TABLE ONLY public.harvest_sets ADD CONSTRAINT fk_rails_0f046d2238 FOREIGN KEY (harvest_source_id) REFERENCES public.harvest_sources(id) ON DELETE CASCADE; +-- +-- Name: depositor_agreements fk_rails_0fc89781d6; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.depositor_agreements + ADD CONSTRAINT fk_rails_0fc89781d6 FOREIGN KEY (submission_target_id) REFERENCES public.submission_targets(id) ON DELETE CASCADE; + + -- -- Name: templates_page_list_instances fk_rails_0fd45e415a; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -14674,6 +14991,14 @@ ALTER TABLE ONLY public.templates_link_list_definitions ADD CONSTRAINT fk_rails_16eb4e2132 FOREIGN KEY (layout_definition_id) REFERENCES public.layouts_main_definitions(id) ON DELETE CASCADE; +-- +-- Name: submission_batch_publication_transitions fk_rails_17b0640306; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.submission_batch_publication_transitions + ADD CONSTRAINT fk_rails_17b0640306 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL; + + -- -- Name: harvest_configurations fk_rails_18ac7e7431; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -14714,6 +15039,14 @@ ALTER TABLE ONLY public.submissions ADD CONSTRAINT fk_rails_1c078fc734 FOREIGN KEY (submission_target_id) REFERENCES public.submission_targets(id) ON DELETE SET NULL; +-- +-- Name: submission_publications fk_rails_1d70496f11; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.submission_publications + ADD CONSTRAINT fk_rails_1d70496f11 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL; + + -- -- Name: schema_version_properties fk_rails_1f31833d7c; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -14754,6 +15087,14 @@ ALTER TABLE ONLY public.harvest_attempt_entity_link_transitions ADD CONSTRAINT fk_rails_248d27a3f5 FOREIGN KEY (harvest_attempt_entity_link_id) REFERENCES public.harvest_attempt_entity_links(id) ON DELETE CASCADE; +-- +-- Name: submission_publications fk_rails_24c0c2f7a9; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.submission_publications + ADD CONSTRAINT fk_rails_24c0c2f7a9 FOREIGN KEY (submission_batch_publication_id) REFERENCES public.submission_batch_publications(id) ON DELETE SET NULL; + + -- -- Name: layouts_hero_instances fk_rails_269b9f8e18; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -14970,6 +15311,22 @@ ALTER TABLE ONLY public.harvest_attempts ADD CONSTRAINT fk_rails_3e309e8f30 FOREIGN KEY (harvest_mapping_id) REFERENCES public.harvest_mappings(id) ON DELETE CASCADE; +-- +-- Name: submission_publications fk_rails_40382ef9d0; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.submission_publications + ADD CONSTRAINT fk_rails_40382ef9d0 FOREIGN KEY (submission_id) REFERENCES public.submissions(id) ON DELETE CASCADE; + + +-- +-- Name: depositor_agreements fk_rails_4078cab068; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.depositor_agreements + ADD CONSTRAINT fk_rails_4078cab068 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + -- -- Name: entities fk_rails_40b78347f2; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -15210,6 +15567,14 @@ ALTER TABLE ONLY public.harvest_mappings ADD CONSTRAINT fk_rails_648247aee2 FOREIGN KEY (harvest_source_id) REFERENCES public.harvest_sources(id) ON DELETE CASCADE; +-- +-- Name: submission_batch_publication_transitions fk_rails_65cbd1e1a5; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.submission_batch_publication_transitions + ADD CONSTRAINT fk_rails_65cbd1e1a5 FOREIGN KEY (submission_batch_publication_id) REFERENCES public.submission_batch_publications(id) ON DELETE CASCADE; + + -- -- Name: templates_detail_instances fk_rails_65e782e7f2; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -15466,6 +15831,14 @@ ALTER TABLE ONLY public.ordering_invalidations ADD CONSTRAINT fk_rails_80f155549c FOREIGN KEY (ordering_id) REFERENCES public.orderings(id) ON DELETE CASCADE; +-- +-- Name: submission_publication_transitions fk_rails_8153002e02; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.submission_publication_transitions + ADD CONSTRAINT fk_rails_8153002e02 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL; + + -- -- Name: entity_links fk_rails_8181666751; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -15650,6 +16023,14 @@ ALTER TABLE ONLY public.submissions ADD CONSTRAINT fk_rails_a1b7a594a4 FOREIGN KEY (schema_version_id) REFERENCES public.schema_versions(id) ON DELETE RESTRICT; +-- +-- Name: depositor_agreement_transitions fk_rails_a2bd1ca49a; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.depositor_agreement_transitions + ADD CONSTRAINT fk_rails_a2bd1ca49a FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL; + + -- -- Name: depositor_request_transitions fk_rails_a2d84de8b8; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -15682,6 +16063,14 @@ ALTER TABLE ONLY public.submissions ADD CONSTRAINT fk_rails_a3d6d36d98 FOREIGN KEY (collection_id) REFERENCES public.collections(id) ON DELETE SET NULL; +-- +-- Name: submission_publication_transitions fk_rails_a4b3b02764; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.submission_publication_transitions + ADD CONSTRAINT fk_rails_a4b3b02764 FOREIGN KEY (submission_publication_id) REFERENCES public.submission_publications(id) ON DELETE CASCADE; + + -- -- Name: items fk_rails_a5b4e81110; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -15714,6 +16103,14 @@ ALTER TABLE ONLY public.assets ADD CONSTRAINT fk_rails_a8a9ebb434 FOREIGN KEY (collection_id) REFERENCES public.collections(id) ON DELETE RESTRICT; +-- +-- Name: submission_batch_publications fk_rails_a92bf6096d; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.submission_batch_publications + ADD CONSTRAINT fk_rails_a92bf6096d FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL; + + -- -- Name: request_steps fk_rails_aaed006f41; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -16130,6 +16527,14 @@ ALTER TABLE ONLY public.harvest_cached_asset_references ADD CONSTRAINT fk_rails_e10303fa2f FOREIGN KEY (harvest_cached_asset_id) REFERENCES public.harvest_cached_assets(id) ON DELETE CASCADE; +-- +-- Name: submission_batch_publications fk_rails_e4fc877234; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.submission_batch_publications + ADD CONSTRAINT fk_rails_e4fc877234 FOREIGN KEY (submission_target_id) REFERENCES public.submission_targets(id) ON DELETE CASCADE; + + -- -- Name: submission_comments fk_rails_e4ff9f0115; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -16353,6 +16758,9 @@ ALTER TABLE ONLY public.templates_ordering_instances SET search_path TO "$user", public; INSERT INTO "schema_migrations" (version) VALUES +('20260312201100'), +('20260312195907'), +('20260312162710'), ('20260227180536'), ('20260227180515'), ('20260227180402'), diff --git a/lib/support/lib/arel_ext/types.rb b/lib/support/lib/arel_ext/types.rb index 13b91587..5ca76e8a 100644 --- a/lib/support/lib/arel_ext/types.rb +++ b/lib/support/lib/arel_ext/types.rb @@ -3,7 +3,7 @@ module Support module ArelExt module Types - include Dry.Types + extend ::Support::Typespace ManyArray = Array.of(Any).constrained(min_size: 2) diff --git a/lib/support/lib/authorization/types.rb b/lib/support/lib/authorization/types.rb index 534adc0e..fc68da25 100644 --- a/lib/support/lib/authorization/types.rb +++ b/lib/support/lib/authorization/types.rb @@ -3,7 +3,7 @@ module Support module Authorization module Types - include Dry.Types + extend ::Support::Typespace # An action ActionName = Coercible::Symbol.constrained(format: /\A[a-z]\w+[a-z]\z/) diff --git a/lib/support/lib/dry_gql/types.rb b/lib/support/lib/dry_gql/types.rb index 1985696c..5466670d 100644 --- a/lib/support/lib/dry_gql/types.rb +++ b/lib/support/lib/dry_gql/types.rb @@ -3,7 +3,7 @@ module Support module DryGQL module Types - include Dry.Types + extend ::Support::Typespace EnumClass = Class.constrained(inherits: ::GraphQL::Schema::Enum) diff --git a/lib/support/lib/dry_mutations/types.rb b/lib/support/lib/dry_mutations/types.rb index 4e2f0a18..6f6ce0ff 100644 --- a/lib/support/lib/dry_mutations/types.rb +++ b/lib/support/lib/dry_mutations/types.rb @@ -3,7 +3,7 @@ module Support module DryMutations module Types - include Dry.Types + extend ::Support::Typespace AttributePath = Array.of(Integer | Coercible::String) end diff --git a/lib/support/lib/frozen_record_helpers/types.rb b/lib/support/lib/frozen_record_helpers/types.rb index b19c73bf..9641b3b1 100644 --- a/lib/support/lib/frozen_record_helpers/types.rb +++ b/lib/support/lib/frozen_record_helpers/types.rb @@ -3,7 +3,7 @@ module Support module FrozenRecordHelpers module Types - include Dry.Types + extend ::Support::Typespace CalculatedAttributes = Hash.map(Coercible::String, Interface(:call)) diff --git a/lib/support/lib/global_types.rb b/lib/support/lib/global_types.rb index fbc822ed..334cbd7f 100644 --- a/lib/support/lib/global_types.rb +++ b/lib/support/lib/global_types.rb @@ -3,9 +3,7 @@ module Support # Types that are shared between support code and application code. module GlobalTypes - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace UUID_PATTERN = /\A[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\z/i diff --git a/lib/support/lib/graphql_api/disable_auth_checks.rb b/lib/support/lib/graphql_api/disable_auth_checks.rb new file mode 100644 index 00000000..bef85164 --- /dev/null +++ b/lib/support/lib/graphql_api/disable_auth_checks.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Support + module GraphQLAPI + # A tremendous amount of time in GraphQL requests is spent on `Controller/GraphQL/Authorized/DynamicFields`, + # and possibly elsewhere. These are not things we authorize. + # + # @see https://github.com/rmosolgo/graphql-ruby/pull/3446 + module DisableAuthChecks + # @note This bypasses auth checks for the type. + def authorized_new(obj, ctx) = new(obj, ctx) + end + end +end diff --git a/lib/support/lib/graphql_api/introspection.rb b/lib/support/lib/graphql_api/introspection.rb new file mode 100644 index 00000000..89897145 --- /dev/null +++ b/lib/support/lib/graphql_api/introspection.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Support + module GraphQLAPI + module Introspection + # A custom `GraphQL::Introspection::DynamicFields` that bypasses auth checks, + # since these are not things we authorize and they use up a lot of request time + # in intermittent bursts. + # + # @see https://github.com/rmosolgo/graphql-ruby/pull/3446 + class DynamicFields < ::GraphQL::Introspection::DynamicFields + extend Support::GraphQLAPI::DisableAuthChecks + end + end + end +end diff --git a/lib/support/lib/hook_based/types.rb b/lib/support/lib/hook_based/types.rb index 81a46553..965cea1f 100644 --- a/lib/support/lib/hook_based/types.rb +++ b/lib/support/lib/hook_based/types.rb @@ -3,7 +3,7 @@ module Support module HookBased module Types - include Dry.Types + extend ::Support::Typespace Attribute = Coercible::Symbol.constrained(format: /\A[a-z]\w*[a-z]\z/).freeze diff --git a/lib/support/lib/models/types.rb b/lib/support/lib/models/types.rb index 8439dcb4..2f245e23 100644 --- a/lib/support/lib/models/types.rb +++ b/lib/support/lib/models/types.rb @@ -4,7 +4,7 @@ module Support module Models # Types specific to working with {ApplicationRecord models}. module Types - include Dry.Types + extend ::Support::Typespace # A Global ID instance or URI. GlobalID = Constructor(GlobalID, GlobalID.method(:parse)).constrained(global_id: true) diff --git a/lib/support/lib/networking/types.rb b/lib/support/lib/networking/types.rb index e18d9972..c0185896 100644 --- a/lib/support/lib/networking/types.rb +++ b/lib/support/lib/networking/types.rb @@ -3,9 +3,7 @@ module Support module Networking module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace RetryCount = Integer.constrained(gt: 0, lt: 11) diff --git a/lib/support/lib/params/types.rb b/lib/support/lib/params/types.rb index b2cc928e..a808b79c 100644 --- a/lib/support/lib/params/types.rb +++ b/lib/support/lib/params/types.rb @@ -4,7 +4,7 @@ module Support module Params # @api private module Types - include Dry.Types + extend ::Support::Typespace Param = Coercible::String diff --git a/lib/support/lib/relay_node/types.rb b/lib/support/lib/relay_node/types.rb index 3bf790df..bf35943c 100644 --- a/lib/support/lib/relay_node/types.rb +++ b/lib/support/lib/relay_node/types.rb @@ -3,7 +3,7 @@ module Support module RelayNode module Types - include Dry.Types + extend ::Support::Typespace # A type that matches an encoded GlobalID that abstracts the structure and renders it # opaque to end users. It is primarily used by GraphQL / Relay as part of the `Node` diff --git a/lib/support/lib/requests/state.rb b/lib/support/lib/requests/state.rb index 5f2ad98d..fef6c26b 100644 --- a/lib/support/lib/requests/state.rb +++ b/lib/support/lib/requests/state.rb @@ -13,11 +13,13 @@ class State around_request :provide_current_state! - around_request :measure! + around_request :measure!, if: :timer? # @return [Support::Requests::Timer, nil] attr_reader :timer + def timer? = timer.present? + # @return [void] def set_up_timer!(...) @timer = Timer.new(...) @@ -35,10 +37,6 @@ def wrap # @return [void] def measure! - # :nocov: - return yield unless timer.present? - # :nocov: - timer.measure! do yield end diff --git a/lib/support/lib/requests/types.rb b/lib/support/lib/requests/types.rb index 4f0e2ce1..f2545619 100644 --- a/lib/support/lib/requests/types.rb +++ b/lib/support/lib/requests/types.rb @@ -4,7 +4,7 @@ module Support module Requests # Request helper types module Types - include Dry.Types + extend ::Support::Typespace Count = Integer.constrained(gteq: 0).default(0).fallback(0) diff --git a/lib/support/lib/schemas/types.rb b/lib/support/lib/schemas/types.rb index 1b842a1e..14fcbe07 100644 --- a/lib/support/lib/schemas/types.rb +++ b/lib/support/lib/schemas/types.rb @@ -3,7 +3,7 @@ module Support module Schemas module Types - include Dry.Types + extend ::Support::Typespace EnumClass = Class.constrained(inherits: ::GraphQL::Schema::Enum) diff --git a/lib/support/lib/statesman_helpers/types.rb b/lib/support/lib/statesman_helpers/types.rb index 86f282e3..b6d7b920 100644 --- a/lib/support/lib/statesman_helpers/types.rb +++ b/lib/support/lib/statesman_helpers/types.rb @@ -3,9 +3,7 @@ module Support module StatesmanHelpers module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace AssociationName = Coercible::Symbol diff --git a/lib/support/lib/typed_sets/types.rb b/lib/support/lib/typed_sets/types.rb index 8d7694cc..60b55da4 100644 --- a/lib/support/lib/typed_sets/types.rb +++ b/lib/support/lib/typed_sets/types.rb @@ -6,7 +6,7 @@ module TypedSets # # @api private module Types - include Dry.Types + extend ::Support::Typespace ConstName = Coercible::Symbol.constrained(format: /\A[A-Z]\w+[a-zA-Z]\z/) diff --git a/lib/support/lib/types.rb b/lib/support/lib/types.rb index dae8a8ef..6e1ba513 100644 --- a/lib/support/lib/types.rb +++ b/lib/support/lib/types.rb @@ -2,9 +2,7 @@ module Support module Types - include Dry.Types - - extend Support::EnhancedTypes + extend ::Support::Typespace # @api private module RelationHelper diff --git a/lib/support/lib/typespace.rb b/lib/support/lib/typespace.rb new file mode 100644 index 00000000..ace61409 --- /dev/null +++ b/lib/support/lib/typespace.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Support + # A module for building namespaced types. + module Typespace + # @return [Module] A cached reference to a `Dry.Types` module. + DRY_TYPES = Dry.Types + + class << self + # @param [Module] mod + # @return [void] + def extended(mod) + super + + mod.include DRY_TYPES + + mod.extend Support::EnhancedTypes + end + end + end +end diff --git a/lib/support/lib/users/anonymous_interface.rb b/lib/support/lib/users/anonymous_interface.rb index 1f6d451a..e64afd83 100644 --- a/lib/support/lib/users/anonymous_interface.rb +++ b/lib/support/lib/users/anonymous_interface.rb @@ -28,6 +28,9 @@ def anonymous? = true # @see User#authenticated? def authenticated? = false + # @return [nil] + def authenticated = nil + def avatar_data = nil def avatar_data=(*); end @@ -97,6 +100,10 @@ def to_encoded_id = Support::System["relay_node.id_from_object"].(self).value! # @return [ActiveSupport::TimeWithZone] def updated_at = NOW + # @!attribute [r] username + # @return [nil] + def username = nil + module ClassMethods # A simulation of `ApplicationRecord.find` to allow {AnonymousUser} to be decoded from a GlobalID. # diff --git a/lib/tasks/yard.rake b/lib/tasks/yard.rake index 451e0d01..c8a4e6db 100644 --- a/lib/tasks/yard.rake +++ b/lib/tasks/yard.rake @@ -21,7 +21,6 @@ begin "--name-tag", "subsystem:Subsystem", "--embed-mixin", "AnonymousInterface", "--embed-mixin", "ClassMethods", - "--embed-mixin", "Shared::EnhancedTypes", "--transitive-tag", "subsystem", "--private", "--protected", diff --git a/spec/factories/depositor_agreements.rb b/spec/factories/depositor_agreements.rb new file mode 100644 index 00000000..05fdb245 --- /dev/null +++ b/spec/factories/depositor_agreements.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :depositor_agreement do + association(:submission_target) + association(:user) + + trait :accepted do + after(:create) do |depositor_agreement, _| + depositor_agreement.transition_to! :accepted + end + end + end +end diff --git a/spec/factories/submission_batch_publications.rb b/spec/factories/submission_batch_publications.rb new file mode 100644 index 00000000..83114f50 --- /dev/null +++ b/spec/factories/submission_batch_publications.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :submission_batch_publication do + transient do + publications_count { 1 } + end + + association(:submission_target) + association(:user) + + after(:create) do |submission_batch_publication, evaluator| + FactoryBot.create_list(:submission_publication, evaluator.publications_count, submission_batch_publication:) + + submission_batch_publication.reload + end + end +end diff --git a/spec/factories/submission_publications.rb b/spec/factories/submission_publications.rb new file mode 100644 index 00000000..64949cd7 --- /dev/null +++ b/spec/factories/submission_publications.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :submission_publication do + submission_batch_publication { nil } + + submission do + next FactoryBot.create(:submission, :approved) unless submission_batch_publication.present? + + submission_target = submission_batch_publication.submission_target + + FactoryBot.create(:submission, :approved, submission_target:) + end + + user do + submission_batch_publication&.user || create(:user) + end + end +end diff --git a/spec/factories/submissions.rb b/spec/factories/submissions.rb index e7e6780f..619a6a12 100644 --- a/spec/factories/submissions.rb +++ b/spec/factories/submissions.rb @@ -8,5 +8,51 @@ association :parent_entity, factory: :collection title { Faker::Lorem.sentence } + + trait :submitted do + after(:create) do |submission| + submission.transition_to! :submitted + end + end + + trait :under_review do + after(:create) do |submission| + submission.transition_to! :submitted + submission.transition_to! :under_review + end + end + + trait :revision_requested do + after(:create) do |submission| + submission.transition_to! :submitted + submission.transition_to! :under_review + submission.transition_to! :revision_requested + end + end + + trait :approved do + after(:create) do |submission| + submission.transition_to! :submitted + submission.transition_to! :under_review + submission.transition_to! :approved + end + end + + trait :rejected do + after(:create) do |submission| + submission.transition_to! :submitted + submission.transition_to! :under_review + submission.transition_to! :rejected + end + end + + trait :published do + after(:create) do |submission| + submission.transition_to! :submitted + submission.transition_to! :under_review + submission.transition_to! :accepted + submission.transition_to! :published + end + end end end diff --git a/spec/factories/token_payload.rb b/spec/factories/token_payload.rb index f0aa5b72..ca51e096 100644 --- a/spec/factories/token_payload.rb +++ b/spec/factories/token_payload.rb @@ -21,7 +21,7 @@ end scope { "openid" } - email_verified { true } + email_verified { user.try(:email_verified) || false } given_name { user&.given_name.presence || Faker::Name.first_name } family_name { user&.family_name.presence || Faker::Name.last_name } name { user&.name || "#{given_name} #{family_name}" } diff --git a/spec/graphql/types/access_management_type_spec.rb b/spec/graphql/types/access_management_type_spec.rb new file mode 100644 index 00000000..bfa013b8 --- /dev/null +++ b/spec/graphql/types/access_management_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::AccessManagementType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :access_management +end diff --git a/spec/graphql/types/asset_kind_type_spec.rb b/spec/graphql/types/asset_kind_type_spec.rb new file mode 100644 index 00000000..5e9983a2 --- /dev/null +++ b/spec/graphql/types/asset_kind_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::AssetKindType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :asset_kind +end diff --git a/spec/graphql/types/contributor_kind_type_spec.rb b/spec/graphql/types/contributor_kind_type_spec.rb new file mode 100644 index 00000000..be15ac76 --- /dev/null +++ b/spec/graphql/types/contributor_kind_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::ContributorKindType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :contributor_kind +end diff --git a/spec/graphql/types/date_precision_type_spec.rb b/spec/graphql/types/date_precision_type_spec.rb new file mode 100644 index 00000000..e28df802 --- /dev/null +++ b/spec/graphql/types/date_precision_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::DatePrecisionType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :date_precision, symbolic: true +end diff --git a/spec/graphql/types/depositor_agreement_state_type_spec.rb b/spec/graphql/types/depositor_agreement_state_type_spec.rb new file mode 100644 index 00000000..a408a2b8 --- /dev/null +++ b/spec/graphql/types/depositor_agreement_state_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::DepositorAgreementStateType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :depositor_agreement_state +end diff --git a/spec/graphql/types/depositor_request_state_type_spec.rb b/spec/graphql/types/depositor_request_state_type_spec.rb new file mode 100644 index 00000000..9e13d3f8 --- /dev/null +++ b/spec/graphql/types/depositor_request_state_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::DepositorRequestStateType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :depositor_request_state +end diff --git a/spec/graphql/types/entity_submission_status_type_spec.rb b/spec/graphql/types/entity_submission_status_type_spec.rb new file mode 100644 index 00000000..a25d34f1 --- /dev/null +++ b/spec/graphql/types/entity_submission_status_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::EntitySubmissionStatusType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :entity_submission_status +end diff --git a/spec/graphql/types/entity_visibility_type_spec.rb b/spec/graphql/types/entity_visibility_type_spec.rb new file mode 100644 index 00000000..3dd525d8 --- /dev/null +++ b/spec/graphql/types/entity_visibility_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::EntityVisibilityType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :entity_visibility +end diff --git a/spec/graphql/types/harvest_schedule_mode_type_spec.rb b/spec/graphql/types/harvest_schedule_mode_type_spec.rb new file mode 100644 index 00000000..ca41ae4f --- /dev/null +++ b/spec/graphql/types/harvest_schedule_mode_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::HarvestScheduleModeType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :harvest_schedule_mode +end diff --git a/spec/graphql/types/permalinkable_kind_type_spec.rb b/spec/graphql/types/permalinkable_kind_type_spec.rb new file mode 100644 index 00000000..4a8d0a2b --- /dev/null +++ b/spec/graphql/types/permalinkable_kind_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::PermalinkableKindType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :permalinkable_kind +end diff --git a/spec/graphql/types/role_kind_type_spec.rb b/spec/graphql/types/role_kind_type_spec.rb new file mode 100644 index 00000000..64be49dd --- /dev/null +++ b/spec/graphql/types/role_kind_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::RoleKindType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :role_kind +end diff --git a/spec/graphql/types/role_primacy_type_spec.rb b/spec/graphql/types/role_primacy_type_spec.rb new file mode 100644 index 00000000..738b3d18 --- /dev/null +++ b/spec/graphql/types/role_primacy_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::RolePrimacyType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :role_primacy +end diff --git a/spec/graphql/types/schema_kind_type_spec.rb b/spec/graphql/types/schema_kind_type_spec.rb new file mode 100644 index 00000000..5fc97ae7 --- /dev/null +++ b/spec/graphql/types/schema_kind_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::SchemaKindType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :schema_kind +end diff --git a/spec/graphql/types/schema_property_function_type_spec.rb b/spec/graphql/types/schema_property_function_type_spec.rb new file mode 100644 index 00000000..34a5a26d --- /dev/null +++ b/spec/graphql/types/schema_property_function_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::SchemaPropertyFunctionType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :schema_property_function +end diff --git a/spec/graphql/types/schema_property_kind_type_spec.rb b/spec/graphql/types/schema_property_kind_type_spec.rb new file mode 100644 index 00000000..c5bb5fd5 --- /dev/null +++ b/spec/graphql/types/schema_property_kind_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::SchemaPropertyKindType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :schema_property_kind +end diff --git a/spec/graphql/types/schema_property_type_type_spec.rb b/spec/graphql/types/schema_property_type_type_spec.rb new file mode 100644 index 00000000..97e6944e --- /dev/null +++ b/spec/graphql/types/schema_property_type_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::SchemaPropertyTypeType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :schema_property_type +end diff --git a/spec/graphql/types/sibling_kind_type_spec.rb b/spec/graphql/types/sibling_kind_type_spec.rb new file mode 100644 index 00000000..575336a9 --- /dev/null +++ b/spec/graphql/types/sibling_kind_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::SiblingKindType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :sibling_kind +end diff --git a/spec/graphql/types/submission_batch_publication_state_type_spec.rb b/spec/graphql/types/submission_batch_publication_state_type_spec.rb new file mode 100644 index 00000000..80cfdd73 --- /dev/null +++ b/spec/graphql/types/submission_batch_publication_state_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::SubmissionBatchPublicationStateType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :submission_batch_publication_state +end diff --git a/spec/graphql/types/submission_comment_role_type_spec.rb b/spec/graphql/types/submission_comment_role_type_spec.rb new file mode 100644 index 00000000..82b37c3d --- /dev/null +++ b/spec/graphql/types/submission_comment_role_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::SubmissionCommentRoleType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :submission_comment_role +end diff --git a/spec/graphql/types/submission_deposit_mode_type_spec.rb b/spec/graphql/types/submission_deposit_mode_type_spec.rb new file mode 100644 index 00000000..11f3d35c --- /dev/null +++ b/spec/graphql/types/submission_deposit_mode_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::SubmissionDepositModeType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :submission_deposit_mode +end diff --git a/spec/graphql/types/submission_publication_state_type_spec.rb b/spec/graphql/types/submission_publication_state_type_spec.rb new file mode 100644 index 00000000..8d31f286 --- /dev/null +++ b/spec/graphql/types/submission_publication_state_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::SubmissionPublicationStateType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :submission_publication_state +end diff --git a/spec/graphql/types/submission_review_state_type_spec.rb b/spec/graphql/types/submission_review_state_type_spec.rb new file mode 100644 index 00000000..4e13d0b4 --- /dev/null +++ b/spec/graphql/types/submission_review_state_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::SubmissionReviewStateType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :submission_review_state +end diff --git a/spec/graphql/types/submission_state_type_spec.rb b/spec/graphql/types/submission_state_type_spec.rb new file mode 100644 index 00000000..f3d4d0ea --- /dev/null +++ b/spec/graphql/types/submission_state_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::SubmissionStateType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :submission_state +end diff --git a/spec/graphql/types/submission_target_state_type_spec.rb b/spec/graphql/types/submission_target_state_type_spec.rb new file mode 100644 index 00000000..2d5ce7ae --- /dev/null +++ b/spec/graphql/types/submission_target_state_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::SubmissionTargetStateType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :submission_target_state +end diff --git a/spec/graphql/types/underlying_data_format_type_spec.rb b/spec/graphql/types/underlying_data_format_type_spec.rb new file mode 100644 index 00000000..72f8d403 --- /dev/null +++ b/spec/graphql/types/underlying_data_format_type_spec.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.describe Types::UnderlyingDataFormatType, type: :graphql_enum do + it_behaves_like "a database-backed graphql enum", :underlying_data_format +end diff --git a/spec/jobs/submission_batch_publications/finish_job_spec.rb b/spec/jobs/submission_batch_publications/finish_job_spec.rb new file mode 100644 index 00000000..2a9baaef --- /dev/null +++ b/spec/jobs/submission_batch_publications/finish_job_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.describe SubmissionBatchPublications::FinishJob, type: :job do + let_it_be(:submission_batch_publication, refind: true) do + FactoryBot.create(:submission_batch_publication) + end + + let(:batch) do + GoodJob::Batch.new.tap do |b| + b.properties[:submission_batch_publication] = submission_batch_publication + end + end + + let(:context) { {} } + + it "transitions the batch publication to finished" do + expect do + described_class.perform_now(batch, context) + end.to change { submission_batch_publication.current_state(force_reload: true) }.to("finished") + end +end diff --git a/spec/jobs/submission_publications/publish_job_spec.rb b/spec/jobs/submission_publications/publish_job_spec.rb new file mode 100644 index 00000000..860dbf5e --- /dev/null +++ b/spec/jobs/submission_publications/publish_job_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.describe SubmissionPublications::PublishJob, type: :job do + let_it_be(:submission_publication, refind: true) { FactoryBot.create(:submission_publication) } + + it_behaves_like "a pass-through operation job", "submission_publications.publish" do + let(:job_arg) { submission_publication } + end +end diff --git a/spec/models/depositor_agreement_spec.rb b/spec/models/depositor_agreement_spec.rb new file mode 100644 index 00000000..1d1c0b19 --- /dev/null +++ b/spec/models/depositor_agreement_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +RSpec.describe DepositorAgreement, type: :model do + let_it_be(:community, refind: true) { FactoryBot.create(:community) } + + let_it_be(:collection, refind: true) { FactoryBot.create(:collection, community:) } + + let_it_be(:item_schema_version, refind: true) { FactoryBot.create(:schema_version, :item) } + + let_it_be(:submission_target, refind: true) do + collection.fetch_submission_target!.tap do |st| + st.configure!(schema_versions: [item_schema_version], deposit_mode: :direct) + st.transition_to! :open + end + end + + let_it_be(:depositor, refind: true) do + FactoryBot.create(:user, depositor_on: collection) + end + + let_it_be(:depositor_agreement, refind: true) do + FactoryBot.create :depositor_agreement, submission_target:, user: depositor + end + + subject { depositor_agreement } + + describe "#accept" do + it "is idempotent" do + expect do + expect(subject.accept).to succeed + end.to change(subject, :state).from("pending").to("accepted") + .and change(subject, :accepted?).from(false).to(true) + .and execute_safely + + expect do + expect(subject.accept).to succeed + end.to keep_the_same(subject, :state) + .and keep_the_same(subject, :accepted?) + .and execute_safely + end + end + + describe "#reset" do + it "has no effect when pending" do + expect(subject).to be_pending + + expect do + expect(subject.reset).to succeed + end.to keep_the_same(subject, :state) + .and keep_the_same(subject, :pending?) + end + + context "when the agreement has been accepted" do + before do + depositor_agreement.transition_to! :accepted + + subject.reload + end + + it "resets the state idempotently" do + expect do + expect(subject.reset).to succeed + end.to change(subject, :state).from("accepted").to("pending") + .and change(subject, :pending?).from(false).to(true) + .and change(subject, :accepted?).from(true).to(false) + + expect do + expect(subject.reset).to succeed + end.to keep_the_same(subject, :state) + .and keep_the_same(subject, :pending?) + .and keep_the_same(subject, :accepted?) + end + end + end + + describe ".reset_all!" do + let_it_be(:accepted_depositor_agreement, refind: true) do + FactoryBot.create(:depositor_agreement, :accepted, submission_target:) + end + + it "resets the accepted agreements" do + expect do + described_class.reset_all! + end.to keep_the_same { depositor_agreement.current_state(force_reload: true) } + .and change { accepted_depositor_agreement.current_state(force_reload: true) }.from("accepted").to("pending") + end + end +end diff --git a/spec/models/submission_target_spec.rb b/spec/models/submission_target_spec.rb index 9219800b..562dbd89 100644 --- a/spec/models/submission_target_spec.rb +++ b/spec/models/submission_target_spec.rb @@ -8,6 +8,22 @@ let_it_be(:submission_target, refind: true) { collection.fetch_submission_target! } + describe "#has_accepted_agreement?" do + it "handles anonymous users" do + expect(submission_target).not_to have_accepted_agreement AnonymousUser.new + end + + it "handles null users" do + expect(submission_target).not_to have_accepted_agreement nil + end + + it "handles un-accepted users" do + user = FactoryBot.create(:user) + + expect(submission_target).not_to have_accepted_agreement user + end + end + context "when the submission target is descendant" do let!(:descendant_submission_target) do submission_target.update!(deposit_mode: :descendant) diff --git a/spec/operations/submission_targets/batch_publish_spec.rb b/spec/operations/submission_targets/batch_publish_spec.rb new file mode 100644 index 00000000..eac0371f --- /dev/null +++ b/spec/operations/submission_targets/batch_publish_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +RSpec.describe SubmissionTargets::BatchPublish, type: :operation do + let_it_be(:community, refind: true) { FactoryBot.create(:community) } + + let_it_be(:collection, refind: true) { FactoryBot.create(:collection, community:) } + + let_it_be(:item_schema_version, refind: true) { FactoryBot.create(:schema_version, :item) } + + let_it_be(:submission_target, refind: true) do + collection.fetch_submission_target!.tap do |st| + st.configure!(schema_versions: [item_schema_version], deposit_mode: :direct) + st.transition_to! :open + end + end + + let_it_be(:approved_submission, refind: true) do + FactoryBot.create(:submission, + :approved, + submission_target:, + schema_version: item_schema_version, + parent_entity: collection, + title: "Test Approved Submission" + ) + end + + let_it_be(:approved_entity, refind: true) { approved_submission.entity } + + let_it_be(:rejected_submission, refind: true) do + FactoryBot.create(:submission, + :rejected, + submission_target:, + schema_version: item_schema_version, + parent_entity: collection, + title: "Test Rejected Submission" + ) + 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] } + + it "enqueues a batch publication" do + expect do + expect_calling_with(submission_target, submissions, user:).to succeed.with(a_kind_of(SubmissionBatchPublication)) + end.to change(SubmissionBatchPublication, :count).by(1) + .and change(SubmissionPublication, :count).by(2) + .and change(SubmissionBatchPublicationTransition.to_pending, :count).by(1) + .and change(SubmissionBatchPublicationTransition.to_batched, :count).by(1) + .and keep_the_same(SubmissionBatchPublicationTransition.to_finished, :count) + .and change(SubmissionPublicationTransition.to_pending, :count).by(2) + .and change(SubmissionPublicationTransition.to_batched, :count).by(2) + .and keep_the_same(SubmissionPublicationTransition.to_success, :count) + .and keep_the_same(SubmissionPublicationTransition.to_failure, :count) + .and have_enqueued_job(SubmissionPublications::PublishJob).twice + + 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) + end +end diff --git a/spec/policies/depositor_agreement_policy_spec.rb b/spec/policies/depositor_agreement_policy_spec.rb new file mode 100644 index 00000000..aec5fa62 --- /dev/null +++ b/spec/policies/depositor_agreement_policy_spec.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +RSpec.describe DepositorAgreementPolicy, type: :policy do + include_context "policy setup" + + let_it_be(:community, refind: true) { FactoryBot.create(:community) } + + let_it_be(:collection, refind: true) { FactoryBot.create(:collection, community:) } + + let_it_be(:item_schema_version, refind: true) { FactoryBot.create(:schema_version, :item) } + + let_it_be(:submission_target, refind: true) do + collection.fetch_submission_target!.tap do |st| + st.configure!(schema_versions: [item_schema_version], deposit_mode: :direct) + st.transition_to! :open + end + end + + let_it_be(:reviewer, refind: true) do + FactoryBot.create(:user).tap do |user| + FactoryBot.create(:submission_target_reviewer, submission_target:, user:) + end.reload + end + + let_it_be(:submitter, refind: true) do + FactoryBot.create(:user, depositor_on: collection) + end + + let_it_be(:depositor_agreement, refind: true) { FactoryBot.create :depositor_agreement, submission_target:, user: submitter } + + let(:record) { depositor_agreement } + + describe_rule :read? do + succeed "as an admin" do + let(:user) { admin } + end + + succeed "as a reviewer" do + let(:user) { reviewer } + end + + succeed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe_rule :show? do + succeed "as an admin" do + let(:user) { admin } + end + + succeed "as a reviewer" do + let(:user) { reviewer } + end + + succeed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe_rule :accept? do + failed "as an admin (who is not the owner)" do + let(:user) { admin } + end + + failed "as a reviewer" do + let(:user) { reviewer } + end + + succeed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + shared_examples_for "no access" do + failed "as an admin" do + let(:user) { admin } + end + + failed "as a reviewer" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe_rule :create? do + include_examples "no access" + end + + describe_rule :update? do + include_examples "no access" + end + + describe_rule :destroy? do + include_examples "no access" + end + + describe "relation scope" do + let(:target) { DepositorAgreement.all } + + subject { policy.apply_scope(target, type: :active_record_relation) } + + context "as an admin" do + let(:user) { admin } + + it "includes everything" do + is_expected.to include record + end + end + + context "as a reviewer" do + let(:user) { reviewer } + + it "includes accessible records" do + is_expected.to include(record) + end + end + + context "as the submitter" do + let(:user) { submitter } + + it "includes accessible records" do + is_expected.to include(record) + end + end + + context "as a regular user" do + let(:user) { regular_user } + + it "forbids access to records" do + is_expected.to exclude(record) + end + end + + context "as an anonymous user" do + let(:user) { anonymous_user } + + it "forbids access to records" do + is_expected.to exclude(record) + end + end + end +end diff --git a/spec/policies/submission_batch_publication_policy_spec.rb b/spec/policies/submission_batch_publication_policy_spec.rb new file mode 100644 index 00000000..0175e248 --- /dev/null +++ b/spec/policies/submission_batch_publication_policy_spec.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +RSpec.describe SubmissionBatchPublicationPolicy, type: :policy do + include_context "policy setup" + + let_it_be(:community, refind: true) { FactoryBot.create(:community) } + + let_it_be(:collection, refind: true) { FactoryBot.create(:collection, community:) } + + let_it_be(:item_schema_version, refind: true) { FactoryBot.create(:schema_version, :item) } + + let_it_be(:submission_target, refind: true) do + collection.fetch_submission_target!.tap do |st| + st.configure!(schema_versions: [item_schema_version], deposit_mode: :direct) + st.transition_to! :open + end + end + + let_it_be(:reviewer, refind: true) do + FactoryBot.create(:user).tap do |user| + FactoryBot.create(:submission_target_reviewer, submission_target:, user:) + end.reload + end + + let_it_be(:submitter, refind: true) do + FactoryBot.create(:user, depositor_on: collection) + end + + let_it_be(:submission, refind: true) do + FactoryBot.create(:submission, + submission_target:, + schema_version: item_schema_version, + parent_entity: collection, + user: submitter, + title: "Test Submission" + ) + end + + let_it_be(:submission_batch_publication, refind: true) { FactoryBot.create :submission_batch_publication, submission_target: } + + let(:record) { submission_batch_publication } + + describe_rule :read? do + succeed "as an admin" do + let(:user) { admin } + end + + succeed "as a reviewer" do + let(:user) { reviewer } + end + + succeed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe_rule :show? do + succeed "as an admin" do + let(:user) { admin } + end + + succeed "as a reviewer" do + let(:user) { reviewer } + end + + succeed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe_rule :create? do + failed "as an admin" do + let(:user) { admin } + end + + failed "as a reviewer" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe_rule :update? do + failed "as an admin" do + let(:user) { admin } + end + + failed "as a reviewer" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe_rule :destroy? do + failed "as an admin" do + let(:user) { admin } + end + + failed "as a reviewer" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe "relation scope" do + let(:target) { SubmissionBatchPublication.all } + + subject { policy.apply_scope(target, type: :active_record_relation) } + + context "as an admin" do + let(:user) { admin } + + it "includes everything" do + is_expected.to include record + end + end + + context "as a reviewer" do + let(:user) { reviewer } + + it "includes everything" do + is_expected.to include record + end + end + + context "as the submitter" do + let(:user) { submitter } + + it "includes everything" do + is_expected.to include record + end + end + + context "as a regular user" do + it "forbids access" do + is_expected.to exclude(record) + end + end + + context "as an anonymous user" do + let(:user) { anonymous_user } + + it "forbids access" do + is_expected.to exclude(record) + end + end + end +end diff --git a/spec/policies/submission_policy_spec.rb b/spec/policies/submission_policy_spec.rb index 956c28ab..f9c9d3bf 100644 --- a/spec/policies/submission_policy_spec.rb +++ b/spec/policies/submission_policy_spec.rb @@ -3,15 +3,54 @@ RSpec.describe SubmissionPolicy, type: :policy do include_context "policy setup" - let_it_be(:submission, refind: true) { FactoryBot.create :submission } + let_it_be(:community, refind: true) { FactoryBot.create(:community) } + + let_it_be(:collection, refind: true) { FactoryBot.create(:collection, community:) } + + let_it_be(:item_schema_version, refind: true) { FactoryBot.create(:schema_version, :item) } + + let_it_be(:submission_target, refind: true) do + collection.fetch_submission_target!.tap do |st| + st.configure!(schema_versions: [item_schema_version], deposit_mode: :direct) + st.transition_to! :open + end + end + + let_it_be(:reviewer, refind: true) do + FactoryBot.create(:user).tap do |user| + FactoryBot.create(:submission_target_reviewer, submission_target:, user:) + end.reload + end + + let_it_be(:submitter, refind: true) do + FactoryBot.create(:user, depositor_on: collection) + end + + let_it_be(:submission, refind: true) do + FactoryBot.create(:submission, + submission_target:, + schema_version: item_schema_version, + parent_entity: collection, + user: submitter, + title: "Test Submission" + ) + end let(:record) { submission } - describe_rule :read? do + shared_examples_for "admin + reviewer + submitter access" do succeed "as an admin" do let(:user) { admin } end + succeed "as a reviewer" do + let(:user) { reviewer } + end + + succeed "as the submitter" do + let(:user) { submitter } + end + failed "as a regular user" do let(:user) { regular_user } end @@ -21,11 +60,19 @@ end end - describe_rule :show? do + shared_examples_for "admin + reviewer access" do succeed "as an admin" do let(:user) { admin } end + succeed "as a reviewer" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + failed "as a regular user" do let(:user) { regular_user } end @@ -35,11 +82,57 @@ end end + shared_examples_for "admin-only access" do + succeed "as an admin" do + let(:user) { admin } + end + + failed "as a reviewer" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe_rule :read? do + include_examples "admin + reviewer + submitter access" + end + + describe_rule :show? do + include_examples "admin + reviewer + submitter access" + end + describe_rule :create? do succeed "as an admin" do let(:user) { admin } end + failed "as a reviewer" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + + succeed "as a submitter who has accepted the agreement" do + let(:user) { submitter } + + before do + submission_target.accept_agreement_for!(user) + end + end + failed "as a regular user" do let(:user) { regular_user } end @@ -54,6 +147,14 @@ let(:user) { admin } end + failed "as a reviewer" do + let(:user) { reviewer } + end + + succeed "as the submitter" do + let(:user) { submitter } + end + failed "as a regular user" do let(:user) { regular_user } end @@ -68,6 +169,14 @@ let(:user) { admin } end + failed "as a reviewer" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + failed "as a regular user" do let(:user) { regular_user } end @@ -77,6 +186,30 @@ end end + describe_rule :alter_schema_version? do + include_examples "admin-only access" + end + + describe_rule :comment? do + include_examples "admin + reviewer + submitter access" + end + + describe_rule :migrate? do + include_examples "admin-only access" + end + + describe_rule :publish? do + include_examples "admin-only access" + end + + describe_rule :request_review? do + include_examples "admin + reviewer + submitter access" + end + + describe_rule :review? do + include_examples "admin + reviewer access" + end + describe "relation scope" do let(:target) { Submission.all } @@ -90,6 +223,22 @@ end end + context "as a reviewer" do + let(:user) { reviewer } + + it "includes accessible records" do + is_expected.to include(record) + end + end + + context "as the submitter" do + let(:user) { submitter } + + it "includes accessible records" do + is_expected.to include(record) + end + end + context "as a regular user" do it "includes accessible records" do is_expected.to exclude(record) diff --git a/spec/policies/submission_publication_policy_spec.rb b/spec/policies/submission_publication_policy_spec.rb new file mode 100644 index 00000000..32141047 --- /dev/null +++ b/spec/policies/submission_publication_policy_spec.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +RSpec.describe SubmissionPublicationPolicy, type: :policy do + include_context "policy setup" + + let_it_be(:community, refind: true) { FactoryBot.create(:community) } + + let_it_be(:collection, refind: true) { FactoryBot.create(:collection, community:) } + + let_it_be(:item_schema_version, refind: true) { FactoryBot.create(:schema_version, :item) } + + let_it_be(:submission_target, refind: true) do + collection.fetch_submission_target!.tap do |st| + st.configure!(schema_versions: [item_schema_version], deposit_mode: :direct) + st.transition_to! :open + end + end + + let_it_be(:reviewer, refind: true) do + FactoryBot.create(:user).tap do |user| + FactoryBot.create(:submission_target_reviewer, submission_target:, user:) + end.reload + end + + let_it_be(:submitter, refind: true) do + FactoryBot.create(:user, depositor_on: collection) + end + + let_it_be(:submission, refind: true) do + FactoryBot.create(:submission, + submission_target:, + schema_version: item_schema_version, + parent_entity: collection, + user: submitter, + title: "Test Submission" + ) + end + + let_it_be(:submission_publication, refind: true) { FactoryBot.create :submission_publication, submission: } + + let(:record) { submission_publication } + + describe_rule :read? do + succeed "as an admin" do + let(:user) { admin } + end + + succeed "as a reviewer" do + let(:user) { reviewer } + end + + succeed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe_rule :show? do + succeed "as an admin" do + let(:user) { admin } + end + + succeed "as a reviewer" do + let(:user) { reviewer } + end + + succeed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe_rule :create? do + failed "as an admin" do + let(:user) { admin } + end + + failed "as a reviewer" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe_rule :update? do + failed "as an admin" do + let(:user) { admin } + end + + failed "as a reviewer" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe_rule :destroy? do + failed "as an admin" do + let(:user) { admin } + end + + failed "as a reviewer" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe "relation scope" do + let(:target) { SubmissionPublication.all } + + subject { policy.apply_scope(target, type: :active_record_relation) } + + context "as an admin" do + let(:user) { admin } + + it "includes everything" do + is_expected.to include record + end + end + + context "as a reviewer" do + let(:user) { reviewer } + + it "includes everything" do + is_expected.to include record + end + end + + context "as the submitter" do + let(:user) { submitter } + + it "includes everything" do + is_expected.to include record + end + end + + context "as a regular user" do + it "forbids access" do + is_expected.to exclude(record) + end + end + + context "as an anonymous user" do + let(:user) { anonymous_user } + + it "forbids access" do + is_expected.to exclude(record) + end + end + end +end diff --git a/spec/policies/submission_target_policy_spec.rb b/spec/policies/submission_target_policy_spec.rb index 0951ef4d..f27f4a2d 100644 --- a/spec/policies/submission_target_policy_spec.rb +++ b/spec/policies/submission_target_policy_spec.rb @@ -3,43 +3,110 @@ RSpec.describe SubmissionTargetPolicy, type: :policy do include_context "policy setup" - let_it_be(:submission_target, refind: true) { FactoryBot.create :submission_target } + let_it_be(:community, refind: true) { FactoryBot.create(:community) } + + let_it_be(:collection, refind: true) { FactoryBot.create(:collection, community:) } + + let_it_be(:item_schema_version, refind: true) { FactoryBot.create(:schema_version, :item) } + + let_it_be(:submission_target, refind: true) do + collection.fetch_submission_target!.tap do |st| + st.configure!(schema_versions: [item_schema_version], deposit_mode: :direct) + st.transition_to! :open + end + end + + let_it_be(:reviewer, refind: true) do + FactoryBot.create(:user).tap do |user| + FactoryBot.create(:submission_target_reviewer, submission_target:, user:) + end.reload + end + + let_it_be(:submitter, refind: true) do + FactoryBot.create(:user, depositor_on: collection) + end let(:record) { submission_target } - describe_rule :read? do + shared_examples_for "no access" do + failed "as an admin" do + let(:user) { admin } + end + + failed "as a reviewer" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do + let(:user) { regular_user } + end + + failed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + shared_examples_for "admin + reviewer + submitter access" do succeed "as an admin" do let(:user) { admin } end - succeed "as a regular user" do + succeed "as a reviewer" do + let(:user) { reviewer } + end + + succeed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do let(:user) { regular_user } end - succeed "as an anonymous user" do + failed "as an anonymous user" do let(:user) { anonymous_user } end end - describe_rule :show? do + shared_examples_for "admin + reviewer access" do succeed "as an admin" do let(:user) { admin } end - succeed "as a regular user" do + succeed "as a reviewer" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + + failed "as a regular user" do let(:user) { regular_user } end - succeed "as an anonymous user" do + failed "as an anonymous user" do let(:user) { anonymous_user } end end - describe_rule :create? do - failed "as an admin" do + shared_examples_for "admin + submitter access" do + succeed "as an admin" do let(:user) { admin } end + failed "as a reviewer" do + let(:user) { reviewer } + end + + succeed "as the submitter" do + let(:user) { submitter } + end + failed "as a regular user" do let(:user) { regular_user } end @@ -49,11 +116,19 @@ end end - describe_rule :update? do + shared_examples_for "admin-only access" do succeed "as an admin" do let(:user) { admin } end + failed "as a reviewer" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + failed "as a regular user" do let(:user) { regular_user } end @@ -63,13 +138,79 @@ end end - describe_rule :destroy? do + shared_examples_for "all access" do + succeed "as an admin" do + let(:user) { admin } + end + + succeed "as a reviewer" do + let(:user) { reviewer } + end + + succeed "as the submitter" do + let(:user) { submitter } + end + + succeed "as a regular user" do + let(:user) { regular_user } + end + + succeed "as an anonymous user" do + let(:user) { anonymous_user } + end + end + + describe_rule :read? do + include_examples "all access" + end + + describe_rule :show? do + include_examples "all access" + end + + describe_rule :deposit? do + include_examples "admin + submitter access" + end + + describe_rule :manage_reviewers? do + include_examples "admin-only access" + end + + describe_rule :publish? do + include_examples "admin-only access" + end + + describe_rule :request_deposit_access? do failed "as an admin" do let(:user) { admin } end - failed "as a regular user" do + succeed "as a reviewer (with no deposit access)" do + let(:user) { reviewer } + end + + failed "as the submitter" do + let(:user) { submitter } + end + + succeed "as a regular user" do + let(:user) { regular_user } + end + + failed "as a regular user who already has a deposit request" do let(:user) { regular_user } + + before do + FactoryBot.create(:depositor_request, submission_target:, user:) + end + end + + failed "as a regular user when the target is closed" do + let(:user) { regular_user } + + before do + submission_target.transition_to! :closed + end end failed "as an anonymous user" do @@ -77,6 +218,26 @@ end end + describe_rule :reset_all_agreements? do + include_examples "admin-only access" + end + + describe_rule :review? do + include_examples "admin + reviewer access" + end + + describe_rule :create? do + include_examples "no access" + end + + describe_rule :update? do + include_examples "admin-only access" + end + + describe_rule :destroy? do + include_examples "no access" + end + describe "relation scope" do let(:target) { SubmissionTarget.all } diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index db3783b0..5f2cf1c1 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -12,7 +12,59 @@ groups.delete "Libraries" groups.delete "Mailers" + depositing_model_bases = %w[ + depositor_agreement + depositor_agreement_transition + depositor_request + depositor_request_transition + submission_batch_publication_transition + submission_batch_publication + submission_comment + submission_deposit_target + submission_publication_transition + submission_publication + submission_review_transition + submission_review + submission_target_reviewer + submission_target_schema_version + submission_target_transition + submission_target + submission_transition + submission + ] + + depositing_models = depositing_model_bases.flat_map do |base| + [ + "app/models/#{base}.rb", + "app/policies/#{base}_policy.rb", + ] + end + [ + "app/models/concerns/submittable.rb", + "app/graphql/types/submittable_type.rb", + %r[app/graphql/mutations/depositor[^/]*\.rb\z], + %r[app/graphql/types/depositor[^/]*\.rb\z], + %r[app/graphql/mutations/submission[^/]*\.rb\z], + %r[app/graphql/types/submission[^/]*\.rb\z], + ] + + depositing_namespaces = depositing_model_bases.map { "#{_1}s" } + + depositing_dirs = depositing_namespaces.flat_map do |namespace| + [ + "app/jobs/#{namespace}", + "app/operations/#{namespace}", + "app/policies/#{namespace}", + "app/services/#{namespace}", + ] + end + + add_group "Depositing", [ + *depositing_models, + *depositing_dirs, + ] + add_group "GraphQL", "app/graphql" + add_group "Harvesting", [ "app/jobs/harvesting", %r|app/models/harvest|, diff --git a/spec/requests/graphql/mutations/depositor_agreement_accept_spec.rb b/spec/requests/graphql/mutations/depositor_agreement_accept_spec.rb new file mode 100644 index 00000000..7bfd531b --- /dev/null +++ b/spec/requests/graphql/mutations/depositor_agreement_accept_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +RSpec.describe Mutations::DepositorAgreementAccept, type: :request, graphql: :mutation, grants_access: true do + mutation_query! <<~GRAPHQL + mutation DepositorAgreementAccept($input: DepositorAgreementAcceptInput!) { + depositorAgreementAccept(input: $input) { + depositorAgreement { + id + state + lastAcceptedAt + + submissionTarget { + id + # check the contextual depositor agreement field on the submission target + depositorAgreement { + id + state + } + } + + canAccept { + ... AuthorizationResultFragment + } + + canReset { + ... AuthorizationResultFragment + } + + transitions { + nodes { + fromState + toState + user { + id + } + } + } + } + + ... ErrorFragment + } + } + GRAPHQL + + let_it_be(:community, refind: true) { FactoryBot.create(:community) } + + let_it_be(:collection, refind: true) { FactoryBot.create(:collection, community:) } + + let_it_be(:item_schema_version, refind: true) { FactoryBot.create(:schema_version, :item) } + + let_it_be(:submission_target, refind: true) do + collection.fetch_submission_target!.tap do |st| + st.configure!(schema_versions: [item_schema_version], deposit_mode: :direct) + st.transition_to! :open + end + end + + let_it_be(:submitter, refind: true) do + FactoryBot.create(:user, depositor_on: collection) + end + + let_mutation_input!(:submission_target_id) { submission_target.to_encoded_id } + + let(:can_accept) { false } + let(:can_reset) { false } + + let(:valid_mutation_shape) do + gql.mutation(:depositor_agreement_accept) do |m| + m.prop :depositor_agreement do |da| + da[:id] = be_an_encoded_id.of_an_existing_model + da[:state] = "ACCEPTED" + da[:last_accepted_at] = be_present + + da.prop :submission_target do |st| + st[:id] = submission_target_id + + st.prop :depositor_agreement do |nda| + nda[:id] = be_an_encoded_id.of_an_existing_model + nda[:state] = "ACCEPTED" + end + end + + da.auth_results(can_accept:, can_reset:) + + da.prop :transitions do |ts| + ts.array :nodes do |ns| + ns.item do |n| + n[:from_state] = "PENDING" + n[:to_state] = "ACCEPTED" + n.prop :user do |u| + u[:id] = current_user.to_encoded_id + end + end + end + end + end + end + end + + let(:empty_mutation_shape) do + gql.empty_mutation :depositor_agreement_accept + end + + shared_examples_for "a successful mutation" do + let(:expected_shape) { valid_mutation_shape } + + it "accepts the depositor agreement" do + expect_request! do |req| + req.effect! change(DepositorAgreement, :count).by(1) + req.effect! change(DepositorAgreementTransition, :count).by(2) + + 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" + end + + as_an_admin_user do + let(:can_reset) { true } + + include_examples "an authorized mutation" + end + + as_a_regular_user do + include_examples "an unauthorized mutation" + + context "when the user has depositor access to the collection" do + let(:current_user) { submitter } + + include_examples "an authorized mutation" + + context "when the agreement has already been accepted" do + before do + submission_target.accept_agreement_for!(current_user) + end + + include_examples "an unauthorized mutation" + end + end + end + + as_an_anonymous_user do + include_examples "an unauthorized mutation" + end +end diff --git a/spec/requests/graphql/mutations/depositor_agreement_reset_all_spec.rb b/spec/requests/graphql/mutations/depositor_agreement_reset_all_spec.rb new file mode 100644 index 00000000..933d462f --- /dev/null +++ b/spec/requests/graphql/mutations/depositor_agreement_reset_all_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +RSpec.describe Mutations::DepositorAgreementResetAll, type: :request, graphql: :mutation do + mutation_query! <<~GRAPHQL + mutation DepositorAgreementResetAll($input: DepositorAgreementResetAllInput!) { + depositorAgreementResetAll(input: $input) { + submissionTarget { + id + } + + ... ErrorFragment + } + } + GRAPHQL + + let_it_be(:community, refind: true) { FactoryBot.create(:community) } + + let_it_be(:collection, refind: true) { FactoryBot.create(:collection, community:) } + + let_it_be(:item_schema_version, refind: true) { FactoryBot.create(:schema_version, :item) } + + let_it_be(:submission_target, refind: true) do + collection.fetch_submission_target!.tap do |st| + st.configure!(schema_versions: [item_schema_version], deposit_mode: :direct) + st.transition_to! :open + end + end + + let_it_be(:submitter, refind: true) do + FactoryBot.create(:user, depositor_on: collection) + end + + let_it_be(:depositor_agreement, refind: true) do + FactoryBot.create(:depositor_agreement, :accepted, submission_target:, user: submitter) + end + + let_mutation_input!(:submission_target_id) { submission_target.to_encoded_id } + + let(:valid_mutation_shape) do + gql.mutation(:depositor_agreement_reset_all) do |m| + m.prop(:submission_target) do |st| + st[:id] = submission_target_id + end + end + end + + let(:empty_mutation_shape) do + gql.empty_mutation :depositor_agreement_reset_all + end + + shared_examples_for "a successful mutation" do + let(:expected_shape) { valid_mutation_shape } + + it "resets all the accepted agreements on the submission target" do + expect_request! do |req| + req.effect! change { depositor_agreement.current_state(force_reload: true) }.from("accepted").to("pending") + + 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" + 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/depositor_agreement_reset_spec.rb b/spec/requests/graphql/mutations/depositor_agreement_reset_spec.rb new file mode 100644 index 00000000..cc8cf2e0 --- /dev/null +++ b/spec/requests/graphql/mutations/depositor_agreement_reset_spec.rb @@ -0,0 +1,174 @@ +# frozen_string_literal: true + +RSpec.describe Mutations::DepositorAgreementReset, type: :request, graphql: :mutation do + mutation_query! <<~GRAPHQL + mutation DepositorAgreementReset($input: DepositorAgreementResetInput!) { + depositorAgreementReset(input: $input) { + depositorAgreement { + id + state + lastAcceptedAt + + submissionTarget { + id + } + + user { + id + } + + canAccept { + ... AuthorizationResultFragment + } + + canReset { + ... AuthorizationResultFragment + } + + transitions { + nodes { + fromState + toState + user { + id + } + } + } + } + + ... ErrorFragment + } + } + GRAPHQL + + let_it_be(:community, refind: true) { FactoryBot.create(:community) } + + let_it_be(:collection, refind: true) { FactoryBot.create(:collection, community:) } + + let_it_be(:item_schema_version, refind: true) { FactoryBot.create(:schema_version, :item) } + + let_it_be(:submission_target, refind: true) do + collection.fetch_submission_target!.tap do |st| + st.configure!(schema_versions: [item_schema_version], deposit_mode: :direct) + st.transition_to! :open + end + end + + let_it_be(:submitter, refind: true) do + FactoryBot.create(:user, depositor_on: collection) + end + + let_mutation_input!(:submission_target_id) { submission_target.to_encoded_id } + + let_mutation_input!(:user_id) { submitter.to_encoded_id } + + let(:can_accept) { false } + let(:can_reset) { false } + + let(:expected_from_state) { nil } + + let(:valid_mutation_shape) do + gql.mutation(:depositor_agreement_reset) do |m| + m.prop :depositor_agreement do |da| + da[:id] = be_an_encoded_id.of_an_existing_model + da[:state] = "PENDING" + + da.prop :submission_target do |st| + st[:id] = submission_target_id + end + + da.prop :user do |u| + u[:id] = user_id + end + + da.auth_results(can_accept:, can_reset:) + + da.prop :transitions do |ts| + ts.array :nodes do |ns| + ns.item do |n| + n[:from_state] = expected_from_state + n[:to_state] = "PENDING" + + n.prop :user do |u| + u[:id] = current_user.to_encoded_id + end + end + end + end + end + end + end + + let(:empty_mutation_shape) do + gql.empty_mutation :depositor_agreement_reset + end + + shared_examples_for "a successful mutation" do + let(:expected_shape) { valid_mutation_shape } + + it "resets the depositor agreement idempotently" do + expect_request! do |req| + req.effect! change(DepositorAgreement, :count).by(1) + req.effect! change(DepositorAgreementTransition.to_pending, :count).by(1) + req.effect! keep_the_same(DepositorAgreementTransition.to_accepted, :count) + + req.data! expected_shape + end + end + + context "when the depositor agreement has been accepted" do + let!(:depositor_agreement) { FactoryBot.create(:depositor_agreement, :accepted, submission_target:, user: submitter) } + + let(:expected_from_state) { "ACCEPTED" } + + it "resets the depositor agreement" do + expect_request! do |req| + req.effect! keep_the_same(DepositorAgreement, :count) + req.effect! change(DepositorAgreementTransition.to_pending, :count).by(1) + req.effect! keep_the_same(DepositorAgreementTransition.to_accepted, :count) + req.effect! change { depositor_agreement.current_state(force_reload: true) }.from("accepted").to("pending") + + req.data! expected_shape + end + 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" + end + + as_an_admin_user do + let(:can_reset) { true } + + include_examples "an authorized mutation" + end + + as_a_regular_user do + include_examples "an unauthorized mutation" + + context "even when the user has depositor access to the collection" do + let(:current_user) { submitter } + + include_examples "an unauthorized mutation" + end + end + + as_an_anonymous_user do + include_examples "an unauthorized mutation" + end +end diff --git a/spec/requests/graphql/mutations/depositor_request_change_state_spec.rb b/spec/requests/graphql/mutations/depositor_request_change_state_spec.rb index bc75bcf9..ec632af1 100644 --- a/spec/requests/graphql/mutations/depositor_request_change_state_spec.rb +++ b/spec/requests/graphql/mutations/depositor_request_change_state_spec.rb @@ -8,6 +8,16 @@ id slug state + + transitions { + nodes { + fromState + toState + user { + id + } + } + } } ... ErrorFragment } @@ -43,12 +53,26 @@ let_mutation_input!(:to_state) { "APPROVED" } + let(:expected_from_state) { "PENDING" } + let(:valid_mutation_shape) do gql.mutation(:depositor_request_change_state) do |m| m.prop(:depositor_request) do |dr| dr[:id] = be_an_encoded_id.of_an_existing_model dr[:slug] = be_an_encoded_slug dr[:state] = to_state + + dr.prop :transitions do |ts| + ts.array :nodes do |ns| + ns.item do |n| + n[:from_state] = expected_from_state + n[:to_state] = to_state + n.prop :user do |u| + u[:id] = current_user.to_encoded_id + end + end + end + end end end end @@ -64,6 +88,9 @@ expect_request! do |req| req.effect! change { existing_depositor_request.current_state(force_reload: true) }.from("pending").to(to_state.downcase) req.effect! change(AccessGrant, :count).by(1) + req.effect! change(DepositorAgreement, :count).by(1) + req.effect! change(DepositorAgreementTransition, :count).by(2) + req.effect! change { submission_target.reload.has_accepted_agreement?(requestor) }.from(false).to(true) req.data! expected_shape end @@ -76,7 +103,14 @@ expect_request! do |req| req.effect! change { existing_depositor_request.current_state(force_reload: true) }.from("pending").to(to_state.downcase) req.effect! keep_the_same(AccessGrant, :count) + req.effect! keep_the_same(DepositorAgreement, :count) + req.effect! keep_the_same(DepositorAgreementTransition, :count) + req.effect! keep_the_same { submission_target.reload.has_accepted_agreement?(requestor) } + + req.data! expected_shape end + + expect(submission_target).not_to have_accepted_agreement(requestor) end end @@ -85,13 +119,22 @@ existing_depositor_request.transition_to! :approved end + let(:expected_from_state) { "APPROVED" } + let(:to_state) { "PENDING" } - it "revokes the access grant" do + it "revokes the access grant but maintains the agreement" do expect_request! do |req| req.effect! change { existing_depositor_request.current_state(force_reload: true) }.from("approved").to("pending") req.effect! change(AccessGrant, :count).by(-1) + req.effect! keep_the_same(DepositorAgreement, :count) + req.effect! keep_the_same(DepositorAgreementTransition, :count) + req.effect! keep_the_same { submission_target.reload.has_accepted_agreement?(requestor) } + + req.data! expected_shape end + + expect(submission_target).to have_accepted_agreement(requestor) end end diff --git a/spec/requests/graphql/mutations/submission_batch_publish_spec.rb b/spec/requests/graphql/mutations/submission_batch_publish_spec.rb new file mode 100644 index 00000000..58c95a0f --- /dev/null +++ b/spec/requests/graphql/mutations/submission_batch_publish_spec.rb @@ -0,0 +1,284 @@ +# frozen_string_literal: true + +RSpec.describe Mutations::SubmissionBatchPublish, type: :request, graphql: :mutation do + mutation_query! <<~GRAPHQL + mutation SubmissionBatchPublish($input: SubmissionBatchPublishInput!) { + submissionBatchPublish(input: $input) { + submissionBatchPublication { + id + state + + publications { + state + + submission { + id + } + + user { + id + } + + transitions { + nodes { + id + + fromState + toState + + user { + id + } + } + } + } + + transitions { + nodes { + id + + fromState + toState + + user { + id + } + } + } + } + + submissionTarget { + id + } + + ... ErrorFragment + } + } + GRAPHQL + + let_it_be(:community, refind: true) { FactoryBot.create(:community) } + + let_it_be(:collection, refind: true) { FactoryBot.create(:collection, community:) } + + let_it_be(:item_schema_version, refind: true) { FactoryBot.create(:schema_version, :item) } + + let_it_be(:submission_target, refind: true) do + collection.fetch_submission_target!.tap do |st| + st.configure!(schema_versions: [item_schema_version], deposit_mode: :direct) + st.transition_to! :open + end + end + + let_it_be(:approved_submission, refind: true) do + FactoryBot.create(:submission, + :approved, + submission_target:, + schema_version: item_schema_version, + parent_entity: collection, + title: "Test Approved Submission" + ) + end + + let_it_be(:approved_entity, refind: true) { approved_submission.entity } + + let_it_be(:rejected_submission, refind: true) do + FactoryBot.create(:submission, + :rejected, + submission_target:, + schema_version: item_schema_version, + parent_entity: collection, + title: "Test Rejected Submission" + ) + end + + let_it_be(:rejected_entity, refind: true) { rejected_submission.entity } + + let_it_be(:unaffiliated_submission, refind: true) do + FactoryBot.create(:submission) + end + + let(:submissions) { [approved_submission] } + + let_mutation_input!(:submission_target_id) { submission_target.to_encoded_id } + + let_mutation_input!(:submission_ids) { submissions.map(&:to_encoded_id) } + + let(:valid_mutation_shape) do + gql.mutation(:submission_batch_publish) do |m| + m.prop :submission_target do |st| + st[:id] = submission_target_id + end + + m.prop :submission_batch_publication do |sbp| + sbp[:id] = be_an_encoded_id.of_an_existing_model + sbp[:state] = "BATCHED" + + sbp.array :publications do |pubs| + submissions.each do |sub| + pubs.item do |pub| + pub[:state] = "BATCHED" + + pub.prop :submission do |s| + s[:id] = sub.to_encoded_id + end + + pub.prop :user do |u| + u[:id] = current_user.to_encoded_id + end + + pub.prop :transitions do |trx| + trx.array :nodes do |ns| + ns.item do |n| + n[:id] = be_an_encoded_id.of_an_existing_model + + n[:from_state] = "PENDING" + n[:to_state] = "BATCHED" + + n.prop :user do |u| + u[:id] = current_user.to_encoded_id + end + end + + ns.item do |n| + n[:id] = be_an_encoded_id.of_an_existing_model + + n[:from_state] = nil + n[:to_state] = "PENDING" + + n.prop :user do |u| + u[:id] = current_user.to_encoded_id + end + end + end + end + end + end + end + + sbp.prop :transitions do |trx| + trx.array :nodes do |ns| + ns.item do |n| + n[:id] = be_an_encoded_id.of_an_existing_model + + n[:from_state] = "PENDING" + n[:to_state] = "BATCHED" + + n.prop :user do |u| + u[:id] = current_user.to_encoded_id + end + end + + ns.item do |n| + n[:id] = be_an_encoded_id.of_an_existing_model + + n[:from_state] = nil + n[:to_state] = "PENDING" + + n.prop :user do |u| + u[:id] = current_user.to_encoded_id + end + end + end + end + end + end + end + + let(:empty_mutation_shape) do + gql.empty_mutation :submission_batch_publish + end + + shared_examples_for "a successful mutation" do + let(:expected_shape) { valid_mutation_shape } + + it "enqueues a batch publishing job for the provided submissions" do + expect_request! do |req| + req.effect! have_enqueued_job(SubmissionPublications::PublishJob).once + req.effect! change(SubmissionBatchPublication, :count).by(1) + req.effect! change(SubmissionPublication, :count).by(1) + + req.data! expected_shape + end + end + + context "when providing an unaffiliated submission" do + let(:submissions) { [approved_submission, unaffiliated_submission] } + + let(:expected_shape) do + gql.mutation(:submission_batch_publish, no_errors: false) do |m| + m[:submission_batch_publication] = be_blank + m[:submission_target] = be_blank + + m.attribute_errors do |ae| + ae.error "submissions.1", :mismatched_batch_submission_target + end + end + end + + it "refuses to enqueue" do + expect_request! do |req| + req.effect! have_enqueued_job(SubmissionPublications::PublishJob).exactly(0).times + req.effect! keep_the_same(SubmissionBatchPublication, :count) + req.effect! keep_the_same(SubmissionPublication, :count) + + req.data! expected_shape + end + end + end + + context "when providing an unpublishable submission" do + let(:submissions) { [approved_submission, rejected_submission] } + + let(:expected_shape) do + gql.mutation(:submission_batch_publish, no_errors: false) do |m| + m[:submission_batch_publication] = be_blank + m[:submission_target] = be_blank + + m.attribute_errors do |ae| + ae.error "submissions.1", :must_be_publishable + end + end + end + + it "refuses to enqueue" do + expect_request! do |req| + req.effect! have_enqueued_job(SubmissionPublications::PublishJob).exactly(0).times + req.effect! keep_the_same(SubmissionBatchPublication, :count) + req.effect! keep_the_same(SubmissionPublication, :count) + + req.data! expected_shape + end + 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" + 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_change_state_spec.rb b/spec/requests/graphql/mutations/submission_change_state_spec.rb index b1dbbabe..68a4be3e 100644 --- a/spec/requests/graphql/mutations/submission_change_state_spec.rb +++ b/spec/requests/graphql/mutations/submission_change_state_spec.rb @@ -184,6 +184,18 @@ include_examples "a failed transition" end + + context "when trying to publish outside of publish mutations" do + let(:to_state) { "PUBLISHED" } + + before do + submission.transition_to! :submitted + submission.transition_to! :under_review + submission.transition_to! :approved + end + + include_examples "an unauthorized mutation" + end end shared_examples_for "an unauthorized mutation" do diff --git a/spec/requests/graphql/mutations/submission_create_spec.rb b/spec/requests/graphql/mutations/submission_create_spec.rb index 7543bc6c..35c3ef82 100644 --- a/spec/requests/graphql/mutations/submission_create_spec.rb +++ b/spec/requests/graphql/mutations/submission_create_spec.rb @@ -126,7 +126,17 @@ grant_access!(depositor_role, on: collection, to: current_user) end - include_examples "an authorized mutation" + context "without an active depositor agreement" do + include_examples "an unauthorized mutation" + end + + context "with an active depositor agreement" do + before do + submission_target.accept_agreement_for!(current_user) + end + + include_examples "an authorized mutation" + end end end diff --git a/spec/requests/graphql/mutations/submission_leave_review_spec.rb b/spec/requests/graphql/mutations/submission_leave_review_spec.rb index 6e295332..2f5c1884 100644 --- a/spec/requests/graphql/mutations/submission_leave_review_spec.rb +++ b/spec/requests/graphql/mutations/submission_leave_review_spec.rb @@ -27,6 +27,18 @@ canDestroy { ... AuthorizationResultFragment } + + transitions { + nodes { + id + fromState + toState + + user { + id + } + } + } } ... ErrorFragment @@ -73,7 +85,7 @@ end m.prop :submission_review do |sr| - sr[:state] = "PENDING" + sr[:state] = to_state sr.auth_results(can_update: true, can_destroy: true) @@ -84,6 +96,21 @@ sr.prop :user do |u| u[:id] = current_user.to_encoded_id end + + sr.prop :transitions do |trs| + trs.array :nodes do |ns| + ns.item do |n| + n[:id] = be_an_encoded_id.of_an_existing_model + + n[:from_state] = "PENDING" + n[:to_state] = to_state + + n.prop :user do |u| + u[:id] = current_user.to_encoded_id + end + end + end + end end end end diff --git a/spec/requests/graphql/mutations/submission_publish_spec.rb b/spec/requests/graphql/mutations/submission_publish_spec.rb new file mode 100644 index 00000000..93ecb2a9 --- /dev/null +++ b/spec/requests/graphql/mutations/submission_publish_spec.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +RSpec.describe Mutations::SubmissionPublish, type: :request, graphql: :mutation do + mutation_query! <<~GRAPHQL + mutation SubmissionPublish($input: SubmissionPublishInput!) { + submissionPublish(input: $input) { + submissionPublication { + id + state + + submission { + id + state + } + + user { + id + } + + transitions { + nodes { + id + fromState + toState + + user { + id + } + } + } + } + + submission { + id + state + } + + entity { + ... on Submittable { + submissionStatus + } + } + + ... ErrorFragment + } + } + GRAPHQL + + let_it_be(:community, refind: true) { FactoryBot.create(:community) } + + let_it_be(:collection, refind: true) { FactoryBot.create(:collection, community:) } + + let_it_be(:item_schema_version, refind: true) { FactoryBot.create(:schema_version, :item) } + + let_it_be(:submission_target, refind: true) do + collection.fetch_submission_target!.tap do |st| + st.configure!(schema_versions: [item_schema_version], deposit_mode: :direct) + st.transition_to! :open + end + end + + let_it_be(:approved_submission, refind: true) do + FactoryBot.create(:submission, + :approved, + submission_target:, + schema_version: item_schema_version, + parent_entity: collection, + title: "Test Approved Submission" + ) + end + + let_it_be(:approved_entity, refind: true) { approved_submission.entity } + + let_it_be(:rejected_submission, refind: true) do + FactoryBot.create(:submission, + :rejected, + submission_target:, + schema_version: item_schema_version, + parent_entity: collection, + title: "Test Rejected Submission" + ) + 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 + gql.mutation(:submission_publish) do |m| + m.prop :entity do |ent| + ent[:submission_status] = "SUBMISSION_PUBLISHED" + end + + m.prop :submission do |s| + s[:id] = submission_id + s[:state] = "PUBLISHED" + end + + m.prop :submission_publication do |sp| + sp[:state] = "SUCCESS" + end + end + end + + let(:empty_mutation_shape) do + gql.empty_mutation :submission_publish + end + + shared_examples_for "a successful mutation" do + let(:expected_shape) { valid_mutation_shape } + + it "publishes the submission" do + expect_request! do |req| + req.effect! change(SubmissionPublication, :count).by(1) + req.effect! change(SubmissionPublicationTransition, :count).by(2) + req.effect! change { approved_submission.current_state(force_reload: true) }.from("approved").to("published") + req.effect! change { approved_entity.reload.submission_status }.from("submission_draft").to("submission_published") + + req.data! expected_shape + end + end + + context "when provided a submission that is not approved" do + let_mutation_input!(:submission_id) { rejected_submission.to_encoded_id } + + let(:expected_shape) do + gql.mutation(:submission_publish, no_errors: false) do |m| + m[:entity] = be_blank + m[:submission] = be_blank + m[:submission_publication] = be_blank + + m.attribute_errors do |ae| + ae.error :submission, :must_be_publishable + end + end + end + + it "refuses to publish" do + expect_request! do |req| + 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 + 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" + 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/query/user_spec.rb b/spec/requests/graphql/query/user_spec.rb index eb10583e..d87704c9 100644 --- a/spec/requests/graphql/query/user_spec.rb +++ b/spec/requests/graphql/query/user_spec.rb @@ -1,62 +1,220 @@ # frozen_string_literal: true RSpec.describe "Query.user", type: :request do - let!(:query) do - <<~GRAPHQL - query getUser($slug: Slug!) { - user(slug: $slug) { - name - slug - avatar { - small { - webp { - url - } - } - } + graphql_query! <<~GRAPHQL + query getUser($slug: Slug!) { + user(slug: $slug) { + ... VOGCommonFields + + accessManagement + globalAdmin + + canReceiveReviewRequests { + ... AuthorizationResultFragment + } + + canRevalidateInstance { + ... AuthorizationResultFragment } } - GRAPHQL - end + } + + fragment VOGCommonFields on User { + id + slug + + name + givenName + familyName + username + email + + allowedActions + anonymous + emailVerified + uploadAccess + uploadToken + + canDestroy { + ... AuthorizationResultFragment + } + + canResetPassword { + ... AuthorizationResultFragment + } + + canUpdate { + ... AuthorizationResultFragment + } + + avatar { + alt + originalFilename + purpose + storage + + metadata { + alt + } + + original { + alt + contentType + originalFilename + storage + } + + hero { + ... ImageSizeFragment + } + + large { + ... ImageSizeFragment + } + + medium { + ... ImageSizeFragment + } + + small { + ... ImageSizeFragment + } + + thumb { + ... ImageSizeFragment + } + } + } + + fragment ImageSizeFragment on ImageSize { + height + size + width + + png { + storage + url + } + + webp { + storage + url + } + } + GRAPHQL let_it_be(:existing_user, refind: true) { FactoryBot.create :user, :with_avatar } - as_an_admin_user do - context "with a valid slug" do - let(:slug) { existing_user.system_slug } + let(:can_destroy) { false } + let(:can_reset_password) { false } + let(:can_receive_review_requests) { false } + let(:can_update) { false } - let!(:graphql_variables) { { slug:, } } + let(:found_user) { nil } - let(:expected_shape) do - gql.query do |q| - q.prop :user do |u| - u[:name] = existing_user.name - u[:slug] = slug - end - end - end + let(:slug) { found_user&.system_slug || random_slug } + + let!(:graphql_variables) { { slug:, } } + + let(:has_upload_access) { found_user&.has_any_upload_access? || false } + + shared_examples_for "a self-lookup" do + context "when looking up self" do + let(:found_user) { current_user } - it "has the right shape" do - expect_request! do |req| - req.data! expected_shape + include_examples "a found user" + end + end + + shared_examples "a found user" do + let(:expected_shape) do + gql.query do |q| + q.prop :user do |u| + u[:name] = found_user.name + u[:slug] = slug + u[:upload_access] = has_upload_access + + u.auth_results(can_destroy:, can_reset_password:, can_receive_review_requests:, can_update:) end end end - context "with an invalid slug" do - let!(:graphql_variables) { { slug: random_slug } } + it "finds the right user" do + expect_request! do |req| + req.data! expected_shape + end + end + end - let(:expected_shape) do - gql.query do |q| - q[:user] = be_blank - end + shared_examples_for "a not found user" do + let(:expected_shape) do + gql.query do |q| + q[:user] = be_blank end + end - it "finds nothing" do - expect_request! do |req| - req.data! expected_shape - end + it "finds nothing" do + expect_request! do |req| + req.data! expected_shape end end end + + as_an_admin_user do + let(:can_reset_password) { true } + let(:can_receive_review_requests) { false } + let(:can_update) { true } + + it_behaves_like "a self-lookup" do + let(:can_receive_review_requests) { true } + let(:has_upload_access) { true } + let(:can_update) { true } + end + + context "against another user" do + let(:found_user) { existing_user } + + it_behaves_like "a found user" + end + + context "with an invalid slug" do + let(:found_user) { nil } + + it_behaves_like "a not found user" + end + end + + as_a_regular_user do + it_behaves_like "a self-lookup" do + let(:can_reset_password) { true } + let(:can_update) { true } + let(:has_upload_access) { false } + end + + context "against another user" do + let(:found_user) { existing_user } + + it_behaves_like "a not found user" + end + + context "with an invalid slug" do + let(:found_user) { nil } + + it_behaves_like "a not found user" + end + end + + as_an_anonymous_user do + context "against another user" do + let(:found_user) { existing_user } + + it_behaves_like "a not found user" + end + + context "with an invalid slug" do + let(:found_user) { nil } + + it_behaves_like "a not found user" + end + end end diff --git a/spec/requests/graphql/query/viewer_spec.rb b/spec/requests/graphql/query/viewer_spec.rb index 721be205..cada8954 100644 --- a/spec/requests/graphql/query/viewer_spec.rb +++ b/spec/requests/graphql/query/viewer_spec.rb @@ -1,33 +1,107 @@ # frozen_string_literal: true RSpec.describe "Query.viewer", type: :request do - let!(:query) do - <<~GRAPHQL - query getViewer { - viewer { - accessManagement - anonymous - allowedActions - id - name - email - emailVerified - globalAdmin - slug - uploadAccess - uploadToken - avatar { - small { - webp { - url - alt - } - } - } + graphql_query! <<~GRAPHQL + query getViewer { + viewer { + ... VOGCommonFields + + accessManagement + globalAdmin + + canReceiveReviewRequests { + ... AuthorizationResultFragment + } + + canRevalidateInstance { + ... AuthorizationResultFragment } } - GRAPHQL - end + } + + fragment VOGCommonFields on User { + id + slug + + name + givenName + familyName + username + email + + allowedActions + anonymous + emailVerified + uploadAccess + uploadToken + + canDestroy { + ... AuthorizationResultFragment + } + + canResetPassword { + ... AuthorizationResultFragment + } + + canUpdate { + ... AuthorizationResultFragment + } + + avatar { + alt + originalFilename + purpose + storage + + metadata { + alt + } + + original { + alt + contentType + originalFilename + storage + } + + hero { + ... ImageSizeFragment + } + + large { + ... ImageSizeFragment + } + + medium { + ... ImageSizeFragment + } + + small { + ... ImageSizeFragment + } + + thumb { + ... ImageSizeFragment + } + } + } + + fragment ImageSizeFragment on ImageSize { + height + size + width + + png { + storage + url + } + + webp { + storage + url + } + } + GRAPHQL let(:expected_upload_token) do current_user.has_any_upload_access? ? be_present : be_nil @@ -37,9 +111,11 @@ let(:expected_access_management) { ::Types::AccessManagementType.name_for_value(current_user.access_management) } + let(:can_receive_review_requests) { false } + let(:expected_shape) do - gql.object do |obj| - obj.prop :viewer do |v| + gql.query do |q| + q.prop :viewer do |v| v[:access_management] = expected_access_management v[:allowed_actions] = current_user.allowed_actions v[:anonymous] = current_user.anonymous? @@ -51,16 +127,14 @@ v[:slug] = current_user.system_slug v[:upload_access] = current_user.has_any_upload_access? v[:upload_token] = expected_upload_token + + v.auth_results(can_receive_review_requests:) end end end - context "as an admin" do - let!(:current_user) { FactoryBot.create :user, :admin, :with_avatar, email_verified: true } - - let(:token) { token_helper.build_token from_user: current_user } - - it "fetches the right values" do + shared_examples_for "a found viewer" do + it "fetches information about the current user" do expect_request! do |req| req.effect! execute_safely @@ -69,15 +143,19 @@ end end - context "as an anonymous user" do - let(:token) { nil } + as_an_admin_user do + let(:can_receive_review_requests) { true } - it "fetches the right values" do - expect_request! do |req| - req.effect! execute_safely + include_examples "a found viewer" + end - req.data! expected_shape - end - end + as_a_regular_user do + let(:can_receive_review_requests) { false } + + include_examples "a found viewer" + end + + as_an_anonymous_user do + include_examples "a found viewer" end end diff --git a/spec/support/shared_examples/a_database_backed_graphql_enum.rb b/spec/support/shared_examples/a_database_backed_graphql_enum.rb index 0fcff270..d2d6f305 100644 --- a/spec/support/shared_examples/a_database_backed_graphql_enum.rb +++ b/spec/support/shared_examples/a_database_backed_graphql_enum.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -RSpec.shared_examples_for "a database-backed graphql enum" do |enum_name| +RSpec.shared_examples_for "a database-backed graphql enum" do |enum_name, symbolic: false| context "within the #{enum_name} PG enum" do - let_it_be(:pg_enum_values) { ApplicationRecord.pg_enum_values(enum_name) } + let_it_be(:pg_enum_values) { ApplicationRecord.pg_enum_values(enum_name).map { |value| symbolic ? value.to_sym : value } } let_it_be(:known_values) { described_class.values.values.map(&:value) } @@ -10,7 +10,7 @@ expect(known_values).to match_array pg_enum_values end - ApplicationRecord.pg_enum_values(enum_name).each do |enum_value| + ApplicationRecord.pg_enum_values(enum_name).map { |value| symbolic ? value.to_sym : value }.each do |enum_value| it "accepts the #{enum_value.inspect} value" do expect(described_class.name_for_value(enum_value)).to be_present end diff --git a/spec/system/lib/types.rb b/spec/system/lib/types.rb index 5a0c4cd5..31a31442 100644 --- a/spec/system/lib/types.rb +++ b/spec/system/lib/types.rb @@ -2,7 +2,7 @@ module Testing module Types - include Dry.Types + extend ::Support::Typespace AnyUser = ::Users::Types::Anonymous | ::Users::Types::Authenticated diff --git a/vendor/random_ip_addresses.yaml b/vendor/random_ip_addresses.yaml deleted file mode 100644 index d5b4432b..00000000 --- a/vendor/random_ip_addresses.yaml +++ /dev/null @@ -1,2002 +0,0 @@ -# The following IP addresses are randomly generated and used for testing purposes only. ---- -- 17.25.55.85 -- 99.126.237.202 -- 174.49.189.83 -- 35.209.220.79 -- 97.98.255.39 -- 165.29.98.166 -- 50.208.18.214 -- 3.132.134.63 -- 207.126.125.33 -- 23.5.77.44 -- 141.123.187.14 -- 99.127.141.28 -- 75.74.152.142 -- 108.220.229.25 -- 71.50.102.31 -- 74.84.7.85 -- 96.253.97.106 -- 71.219.116.243 -- 17.204.46.168 -- 158.121.203.221 -- 176.118.82.202 -- 165.189.201.171 -- 204.85.5.235 -- 24.164.172.161 -- 99.146.254.107 -- 96.28.28.211 -- 146.229.254.48 -- 99.118.47.208 -- 104.227.176.245 -- 173.138.47.45 -- 96.71.119.11 -- 35.89.252.192 -- 76.86.93.69 -- 54.204.202.49 -- 52.182.206.187 -- 70.125.83.241 -- 71.167.249.193 -- 104.53.140.67 -- 70.177.32.152 -- 34.201.129.203 -- 50.74.214.140 -- 23.236.73.84 -- 146.95.116.86 -- 200.62.23.217 -- 50.205.53.56 -- 129.210.119.153 -- 66.153.237.92 -- 66.119.210.254 -- 73.238.15.182 -- 199.188.139.81 -- 68.208.86.78 -- 174.145.61.18 -- 107.168.22.67 -- 184.23.82.215 -- 71.74.172.181 -- 68.96.144.4 -- 99.177.160.219 -- 147.73.102.202 -- 73.241.191.33 -- 12.86.216.239 -- 162.231.84.46 -- 162.210.244.102 -- 3.93.82.65 -- 50.24.212.147 -- 173.152.240.34 -- 184.247.83.74 -- 209.211.118.151 -- 207.174.240.159 -- 73.37.192.31 -- 99.103.240.220 -- 165.29.94.53 -- 72.43.109.115 -- 75.71.134.212 -- 35.164.183.29 -- 65.211.14.209 -- 66.158.126.227 -- 23.34.53.138 -- 52.219.99.172 -- 75.108.232.242 -- 4.79.153.60 -- 107.203.26.224 -- 117.51.108.117 -- 136.43.113.132 -- 64.39.216.180 -- 209.147.132.4 -- 173.215.103.197 -- 75.50.97.87 -- 96.40.215.104 -- 149.4.70.173 -- 207.193.61.31 -- 162.233.171.210 -- 34.133.57.32 -- 12.13.54.251 -- 3.86.80.6 -- 18.205.211.68 -- 206.221.140.254 -- 64.119.4.174 -- 138.156.0.29 -- 74.124.37.105 -- 18.118.164.223 -- 72.6.19.202 -- 34.160.80.182 -- 104.214.13.39 -- 148.107.129.69 -- 208.78.251.104 -- 12.227.86.213 -- 76.23.136.83 -- 44.198.111.80 -- 67.22.15.57 -- 63.236.75.227 -- 70.34.170.27 -- 67.220.23.237 -- 98.156.7.255 -- 107.210.222.229 -- 38.106.204.89 -- 129.237.227.63 -- 73.11.235.244 -- 3.218.178.108 -- 74.71.2.233 -- 67.188.118.215 -- 38.101.160.183 -- 207.225.99.181 -- 104.15.22.182 -- 35.186.100.45 -- 174.72.116.73 -- 47.202.106.242 -- 47.200.198.45 -- 24.44.210.1 -- 40.112.230.93 -- 128.238.17.118 -- 35.219.140.10 -- 100.38.37.20 -- 65.196.19.116 -- 75.135.25.38 -- 131.215.74.137 -- 67.246.30.232 -- 99.137.172.230 -- 108.198.212.104 -- 184.172.251.131 -- 166.127.30.74 -- 54.69.137.50 -- 68.12.245.190 -- 40.138.36.18 -- 12.14.1.98 -- 104.235.241.39 -- 73.86.255.235 -- 34.111.130.207 -- 70.163.40.96 -- 108.72.23.154 -- 208.106.46.40 -- 73.67.180.149 -- 169.48.161.150 -- 75.182.225.99 -- 20.44.114.141 -- 76.18.199.178 -- 74.203.112.65 -- 63.144.25.10 -- 12.222.134.77 -- 164.67.147.201 -- 66.19.1.67 -- 107.0.23.51 -- 98.51.15.190 -- 150.210.240.65 -- 129.255.147.233 -- 72.107.133.13 -- 168.29.237.117 -- 13.85.128.239 -- 131.216.171.35 -- 34.138.51.43 -- 32.209.168.94 -- 12.41.27.1 -- 67.255.111.86 -- 67.176.60.158 -- 98.202.200.114 -- 23.30.243.135 -- 154.28.254.181 -- 216.60.251.35 -- 108.203.215.68 -- 76.2.237.135 -- 104.168.28.31 -- 108.48.204.81 -- 68.133.69.70 -- 18.8.91.45 -- 140.226.49.160 -- 99.60.126.204 -- 70.8.36.92 -- 107.129.156.86 -- 173.52.37.41 -- 50.23.72.1 -- 131.106.40.237 -- 99.126.39.32 -- 66.171.88.241 -- 47.18.235.253 -- 130.65.96.112 -- 96.72.183.187 -- 207.50.153.198 -- 54.212.105.248 -- 98.250.50.187 -- 44.194.8.18 -- 71.184.109.187 -- 194.233.146.165 -- 44.244.117.143 -- 208.124.66.166 -- 47.227.208.29 -- 23.242.77.150 -- 18.214.233.24 -- 75.73.46.203 -- 35.10.65.196 -- 47.181.215.131 -- 158.121.128.212 -- 12.13.211.130 -- 71.218.137.220 -- 134.241.173.175 -- 12.97.32.228 -- 34.75.82.237 -- 23.23.79.146 -- 70.239.86.73 -- 136.40.8.15 -- 108.249.254.9 -- 73.94.14.70 -- 66.26.25.218 -- 172.116.13.121 -- 20.57.96.69 -- 98.29.58.146 -- 20.190.212.183 -- 137.118.152.207 -- 143.236.143.137 -- 63.66.111.91 -- 104.240.13.52 -- 173.110.65.201 -- 96.87.245.151 -- 71.130.225.120 -- 108.208.90.157 -- 50.252.30.75 -- 70.42.151.32 -- 204.253.66.3 -- 107.80.49.94 -- 184.188.207.126 -- 98.27.39.16 -- 174.127.38.97 -- 76.86.238.188 -- 143.226.104.249 -- 98.127.17.196 -- 107.215.19.43 -- 63.45.14.166 -- 172.225.30.104 -- 73.58.215.98 -- 75.60.234.54 -- 20.242.83.102 -- 207.28.228.103 -- 24.19.60.84 -- 50.25.217.224 -- 69.46.227.48 -- 205.213.34.213 -- 52.90.74.186 -- 99.196.212.107 -- 104.180.81.27 -- 139.67.36.2 -- 47.206.152.122 -- 34.127.20.238 -- 52.205.162.154 -- 67.205.170.174 -- 12.8.23.141 -- 149.142.86.54 -- 209.174.151.21 -- 69.180.182.82 -- 73.140.123.163 -- 130.157.51.0 -- 184.16.234.214 -- 35.150.186.86 -- 34.203.143.35 -- 71.134.153.189 -- 184.155.85.61 -- 96.241.247.117 -- 74.69.62.93 -- 98.238.112.205 -- 52.205.137.160 -- 20.141.174.45 -- 205.251.94.83 -- 44.195.164.183 -- 140.160.63.75 -- 128.206.223.153 -- 68.67.96.162 -- 20.57.58.7 -- 194.36.37.39 -- 4.31.164.146 -- 12.154.149.109 -- 98.122.121.61 -- 104.55.81.175 -- 71.221.64.140 -- 65.60.214.88 -- 173.80.231.68 -- 99.4.164.8 -- 147.129.141.64 -- 165.189.65.156 -- 104.181.66.191 -- 208.198.43.194 -- 17.27.123.204 -- 64.130.178.199 -- 68.109.79.239 -- 38.145.154.135 -- 18.219.101.128 -- 35.93.55.147 -- 71.226.217.21 -- 204.133.33.156 -- 45.50.141.39 -- 34.208.108.74 -- 107.27.181.36 -- 66.144.72.89 -- 146.209.145.242 -- 204.251.169.81 -- 69.47.247.246 -- 76.113.2.15 -- 68.103.47.109 -- 104.169.92.101 -- 129.57.33.237 -- 12.191.86.10 -- 209.50.112.171 -- 69.74.236.192 -- 96.224.243.85 -- 128.211.220.167 -- 12.163.86.181 -- 74.36.187.144 -- 12.181.96.11 -- 209.51.86.240 -- 74.39.43.80 -- 204.46.76.62 -- 165.234.14.248 -- 76.118.57.146 -- 66.252.59.229 -- 107.193.200.157 -- 99.27.193.243 -- 73.38.84.66 -- 139.127.25.130 -- 104.97.107.136 -- 174.146.247.201 -- 134.250.243.203 -- 131.212.240.201 -- 162.96.105.156 -- 70.92.74.138 -- 108.202.201.132 -- 71.208.255.69 -- 159.230.227.79 -- 97.122.250.113 -- 12.246.246.75 -- 20.55.43.88 -- 99.206.157.121 -- 73.196.107.136 -- 54.67.88.19 -- 172.85.217.235 -- 99.104.120.233 -- 96.230.148.151 -- 44.204.221.132 -- 71.40.112.63 -- 184.216.32.217 -- 174.20.51.171 -- 128.14.134.180 -- 47.19.79.200 -- 184.232.98.228 -- 160.87.172.121 -- 67.41.167.141 -- 159.115.229.68 -- 23.122.246.239 -- 99.98.154.65 -- 135.134.141.174 -- 71.142.53.5 -- 71.93.179.220 -- 32.208.250.163 -- 216.68.241.112 -- 72.209.185.117 -- 38.43.52.104 -- 215.71.69.147 -- 146.111.245.130 -- 174.101.40.55 -- 50.51.252.80 -- 155.247.99.45 -- 108.68.132.150 -- 213.173.38.65 -- 173.218.109.115 -- 208.63.24.47 -- 18.30.33.172 -- 38.2.49.244 -- 184.56.243.182 -- 76.255.30.54 -- 72.214.102.214 -- 20.112.232.107 -- 139.62.248.218 -- 98.150.203.181 -- 76.203.113.111 -- 67.78.159.97 -- 199.87.95.19 -- 209.119.173.17 -- 73.59.232.139 -- 18.233.110.155 -- 66.218.54.71 -- 107.208.226.109 -- 63.142.248.193 -- 45.86.60.242 -- 98.177.34.159 -- 134.161.116.201 -- 70.166.186.14 -- 76.191.18.30 -- 35.229.52.48 -- 164.54.53.222 -- 55.2.21.55 -- 70.11.78.219 -- 98.242.13.16 -- 50.199.143.79 -- 64.34.180.165 -- 97.71.67.127 -- 73.185.150.218 -- 71.130.166.236 -- 173.252.189.131 -- 157.142.239.146 -- 147.73.225.39 -- 99.74.113.30 -- 73.148.183.130 -- 68.43.226.118 -- 174.62.27.131 -- 198.56.236.54 -- 73.69.39.116 -- 65.58.37.223 -- 3.239.216.218 -- 54.159.170.121 -- 104.152.46.145 -- 67.255.64.2 -- 129.146.20.251 -- 162.80.0.101 -- 76.243.11.222 -- 174.30.75.209 -- 73.86.3.144 -- 24.63.163.220 -- 170.173.18.169 -- 71.45.203.44 -- 71.204.148.65 -- 12.33.10.42 -- 147.185.193.44 -- 13.14.130.207 -- 3.83.155.33 -- 67.8.220.224 -- 104.246.126.192 -- 108.88.240.238 -- 34.199.19.166 -- 40.132.221.99 -- 157.89.120.170 -- 209.51.178.111 -- 68.203.158.190 -- 63.150.239.225 -- 198.11.207.109 -- 12.110.245.21 -- 34.171.117.56 -- 137.128.30.246 -- 173.17.64.214 -- 47.86.172.117 -- 75.103.30.40 -- 151.154.5.52 -- 96.253.3.124 -- 107.129.71.154 -- 72.239.86.209 -- 72.189.74.81 -- 99.172.98.58 -- 73.46.182.245 -- 71.32.116.126 -- 100.17.120.142 -- 66.102.152.249 -- 76.237.27.18 -- 216.186.8.137 -- 174.49.236.0 -- 204.65.114.125 -- 184.2.74.57 -- 76.174.173.5 -- 76.127.94.109 -- 18.214.75.217 -- 71.88.191.239 -- 76.180.61.145 -- 192.175.50.196 -- 66.55.59.12 -- 65.60.178.189 -- 12.21.3.108 -- 74.218.178.43 -- 149.162.9.37 -- 184.209.30.107 -- 160.94.240.7 -- 85.92.156.45 -- 73.85.81.105 -- 192.34.111.239 -- 71.206.222.188 -- 73.4.114.17 -- 134.84.58.141 -- 96.28.251.113 -- 99.74.139.95 -- 63.153.21.126 -- 50.123.183.80 -- 44.217.60.125 -- 76.137.9.226 -- 74.124.112.200 -- 69.212.204.14 -- 66.30.162.50 -- 104.52.38.19 -- 72.58.206.173 -- 96.227.108.5 -- 24.98.204.39 -- 23.55.51.155 -- 76.245.222.18 -- 130.35.172.9 -- 128.230.73.140 -- 24.216.118.185 -- 172.72.83.43 -- 131.210.237.60 -- 75.63.170.123 -- 12.158.146.172 -- 136.50.10.183 -- 161.155.120.55 -- 184.56.148.83 -- 70.62.111.237 -- 73.124.104.151 -- 65.199.220.144 -- 15.181.101.183 -- 139.78.252.204 -- 173.117.211.190 -- 52.34.188.176 -- 209.90.72.237 -- 13.66.8.204 -- 12.30.85.141 -- 173.115.17.76 -- 162.203.207.179 -- 184.75.17.147 -- 141.211.1.254 -- 139.169.179.74 -- 73.55.62.178 -- 204.9.159.252 -- 24.223.180.134 -- 208.107.206.245 -- 98.151.147.50 -- 173.64.107.78 -- 66.79.214.139 -- 69.89.204.128 -- 98.215.245.216 -- 174.80.129.38 -- 207.73.22.43 -- 168.229.88.90 -- 73.205.202.181 -- 63.247.158.18 -- 20.55.91.206 -- 140.186.74.142 -- 205.255.226.32 -- 76.98.250.225 -- 208.226.133.74 -- 64.44.26.241 -- 207.74.96.0 -- 166.177.194.235 -- 108.228.160.91 -- 75.74.192.197 -- 72.228.181.252 -- 23.108.115.182 -- 54.145.14.212 -- 23.199.65.74 -- 44.211.8.23 -- 47.219.131.180 -- 155.47.91.165 -- 209.152.39.219 -- 70.0.170.80 -- 64.156.219.55 -- 156.145.83.159 -- 146.166.190.182 -- 76.250.216.90 -- 34.204.8.21 -- 168.122.17.92 -- 160.94.101.106 -- 13.86.200.117 -- 98.136.217.109 -- 68.126.133.148 -- 149.152.179.163 -- 66.25.233.171 -- 160.94.72.116 -- 70.210.135.197 -- 52.10.76.212 -- 166.177.115.159 -- 140.141.23.149 -- 184.88.67.222 -- 52.202.75.221 -- 68.4.160.177 -- 35.243.200.236 -- 207.190.43.105 -- 73.173.88.197 -- 68.97.152.156 -- 167.89.195.14 -- 69.66.170.151 -- 209.210.74.152 -- 69.124.89.55 -- 216.24.127.181 -- 156.248.230.193 -- 75.68.237.116 -- 97.78.229.200 -- 184.254.127.120 -- 108.112.184.62 -- 8.29.185.31 -- 65.208.178.50 -- 23.220.42.85 -- 204.250.6.147 -- 20.88.33.200 -- 73.18.32.115 -- 72.182.212.84 -- 162.96.176.182 -- 70.199.213.26 -- 34.210.118.201 -- 68.189.12.166 -- 72.130.255.169 -- 71.72.7.38 -- 20.88.181.168 -- 67.181.121.80 -- 76.95.81.39 -- 4.7.231.2 -- 184.232.20.110 -- 128.235.1.235 -- 74.66.153.36 -- 73.61.55.86 -- 12.227.53.209 -- 184.16.192.35 -- 174.68.223.248 -- 38.94.139.206 -- 12.168.207.27 -- 108.190.45.144 -- 73.42.77.5 -- 12.220.119.242 -- 129.79.246.4 -- 108.198.129.17 -- 70.114.246.3 -- 35.147.187.166 -- 68.73.81.232 -- 170.184.64.247 -- 70.98.31.255 -- 3.83.226.98 -- 72.106.57.37 -- 205.155.22.101 -- 184.18.38.195 -- 107.219.203.88 -- 68.40.246.74 -- 206.211.156.98 -- 141.151.86.88 -- 67.136.238.115 -- 24.250.156.250 -- 130.68.114.214 -- 73.247.57.248 -- 20.114.210.51 -- 99.191.8.128 -- 134.6.185.228 -- 216.171.242.59 -- 184.100.185.24 -- 69.223.165.36 -- 208.98.50.18 -- 140.198.89.23 -- 71.233.232.11 -- 147.126.17.170 -- 130.160.46.217 -- 55.75.196.92 -- 162.80.43.39 -- 64.101.164.227 -- 174.77.86.14 -- 174.64.164.238 -- 162.22.240.115 -- 97.100.56.95 -- 75.111.48.161 -- 74.131.2.142 -- 34.74.251.52 -- 96.225.107.50 -- 55.23.180.113 -- 198.49.53.128 -- 216.189.190.15 -- 52.113.135.188 -- 23.211.32.119 -- 34.192.192.140 -- 174.25.4.182 -- 52.186.110.35 -- 96.82.154.228 -- 173.139.32.9 -- 135.134.117.151 -- 108.75.3.63 -- 184.63.193.155 -- 205.232.83.139 -- 99.203.165.230 -- 18.9.62.118 -- 205.127.187.117 -- 24.91.48.140 -- 158.62.137.14 -- 98.227.217.212 -- 130.64.206.209 -- 67.211.175.37 -- 54.145.187.255 -- 12.110.65.96 -- 195.155.2.232 -- 207.106.19.31 -- 47.202.73.64 -- 74.70.43.151 -- 156.12.89.59 -- 165.123.50.204 -- 64.118.181.108 -- 98.206.126.18 -- 24.140.130.216 -- 71.81.9.104 -- 35.231.214.230 -- 73.68.221.4 -- 150.212.144.97 -- 209.232.69.150 -- 75.130.46.89 -- 209.61.199.187 -- 131.62.123.70 -- 52.126.102.155 -- 130.254.160.134 -- 162.252.143.136 -- 98.206.207.173 -- 63.225.220.1 -- 69.229.242.196 -- 12.204.26.84 -- 204.111.223.12 -- 18.214.10.59 -- 18.211.107.10 -- 38.84.3.179 -- 20.106.151.128 -- 129.65.38.112 -- 75.149.239.35 -- 73.201.138.140 -- 129.113.165.222 -- 72.134.92.160 -- 86.109.10.6 -- 50.222.61.77 -- 66.199.108.187 -- 54.242.113.167 -- 76.204.62.224 -- 199.146.60.12 -- 108.5.176.50 -- 164.114.67.55 -- 99.125.108.6 -- 216.170.128.47 -- 70.145.252.138 -- 64.93.218.182 -- 3.225.67.85 -- 34.71.103.121 -- 198.72.182.43 -- 107.53.31.133 -- 184.170.105.76 -- 141.106.143.128 -- 70.175.225.150 -- 69.50.151.190 -- 157.242.151.142 -- 74.174.134.15 -- 73.208.139.141 -- 24.211.94.55 -- 99.65.103.4 -- 50.30.147.212 -- 216.58.130.63 -- 184.159.109.108 -- 172.87.32.96 -- 128.2.55.173 -- 69.115.110.8 -- 142.154.198.56 -- 74.33.181.128 -- 209.193.82.97 -- 67.63.106.141 -- 69.27.23.200 -- 52.20.205.36 -- 70.57.1.195 -- 54.241.33.207 -- 108.205.138.60 -- 98.122.61.140 -- 104.155.162.53 -- 107.138.140.54 -- 8.12.19.79 -- 66.139.155.204 -- 208.87.124.218 -- 38.70.10.225 -- 73.209.59.104 -- 24.121.56.223 -- 96.7.181.239 -- 173.2.216.254 -- 207.223.113.249 -- 57.69.155.94 -- 72.235.141.108 -- 174.199.225.179 -- 68.247.242.218 -- 30.147.41.196 -- 128.160.44.79 -- 20.97.239.74 -- 50.24.148.106 -- 52.204.128.248 -- 70.114.80.191 -- 67.230.229.11 -- 160.36.135.47 -- 44.217.92.62 -- 129.150.222.41 -- 209.210.95.250 -- 68.53.154.98 -- 35.173.137.203 -- 146.79.230.6 -- 45.24.105.9 -- 24.197.127.160 -- 107.48.100.213 -- 160.94.50.10 -- 99.29.138.7 -- 52.177.130.22 -- 71.215.242.189 -- 98.168.6.50 -- 174.18.120.248 -- 108.50.140.250 -- 68.200.82.166 -- 173.54.99.78 -- 3.13.91.250 -- 85.153.97.235 -- 104.51.203.198 -- 156.63.150.50 -- 152.3.125.0 -- 64.214.227.233 -- 99.161.155.73 -- 137.112.156.120 -- 108.220.110.68 -- 98.114.96.147 -- 99.34.234.12 -- 184.224.182.41 -- 71.230.151.93 -- 199.189.199.200 -- 99.113.151.122 -- 96.255.58.243 -- 23.106.15.169 -- 99.106.128.183 -- 173.22.64.157 -- 209.173.169.49 -- 63.85.53.126 -- 63.145.57.89 -- 172.118.153.252 -- 66.117.57.239 -- 71.40.154.9 -- 65.197.35.160 -- 69.194.184.144 -- 12.147.202.218 -- 98.52.208.102 -- 71.81.40.184 -- 3.236.114.138 -- 209.37.65.20 -- 99.95.252.137 -- 99.10.210.186 -- 71.221.90.104 -- 172.104.218.224 -- 75.212.200.213 -- 174.151.136.44 -- 98.148.209.243 -- 209.178.218.185 -- 75.128.240.10 -- 72.219.55.200 -- 67.14.209.187 -- 208.94.74.41 -- 168.245.147.125 -- 207.91.166.228 -- 70.98.254.113 -- 73.200.122.255 -- 24.39.168.138 -- 134.67.173.189 -- 139.55.31.218 -- 71.87.18.51 -- 65.75.56.225 -- 128.12.125.88 -- 99.197.179.236 -- 24.5.53.119 -- 4.59.231.118 -- 198.20.176.232 -- 52.253.12.247 -- 75.39.105.28 -- 74.142.179.249 -- 134.50.52.193 -- 104.226.14.174 -- 76.204.31.224 -- 44.196.141.204 -- 67.106.43.116 -- 4.7.100.111 -- 38.128.95.67 -- 152.42.108.133 -- 204.213.192.79 -- 129.198.16.14 -- 108.3.130.192 -- 38.39.210.173 -- 184.169.215.10 -- 161.6.43.82 -- 136.234.31.211 -- 66.176.5.35 -- 66.229.209.129 -- 169.53.28.42 -- 208.117.195.6 -- 174.77.171.42 -- 74.218.206.25 -- 205.120.61.19 -- 209.34.117.64 -- 173.132.165.243 -- 35.93.68.119 -- 128.105.150.140 -- 108.222.191.1 -- 75.108.154.17 -- 108.67.37.12 -- 66.23.212.3 -- 96.78.36.107 -- 108.91.48.198 -- 23.250.32.20 -- 136.53.114.81 -- 35.186.164.178 -- 107.34.132.13 -- 12.38.78.2 -- 73.219.22.3 -- 154.208.12.132 -- 199.195.209.147 -- 164.64.101.115 -- 98.125.242.227 -- 216.6.133.138 -- 52.71.198.39 -- 169.237.133.158 -- 208.226.132.242 -- 99.29.193.140 -- 18.7.75.180 -- 74.73.74.26 -- 38.78.155.118 -- 20.127.198.185 -- 50.242.151.170 -- 128.187.16.102 -- 96.19.187.175 -- 160.3.107.150 -- 23.228.118.231 -- 64.111.249.217 -- 76.27.68.138 -- 216.81.202.120 -- 47.217.9.171 -- 97.112.201.127 -- 34.236.90.39 -- 75.106.51.64 -- 35.94.39.21 -- 71.112.254.81 -- 128.226.175.168 -- 74.7.142.1 -- 72.240.54.84 -- 12.163.58.43 -- 173.109.205.138 -- 108.69.213.156 -- 216.183.72.255 -- 47.189.0.25 -- 50.234.113.208 -- 108.18.202.253 -- 151.112.66.177 -- 67.165.110.223 -- 66.65.39.23 -- 206.125.78.178 -- 74.68.60.4 -- 55.94.23.139 -- 40.74.179.9 -- 24.177.42.139 -- 12.53.19.12 -- 97.46.94.183 -- 104.181.80.161 -- 12.1.253.162 -- 173.203.53.235 -- 50.196.209.141 -- 174.208.67.203 -- 73.154.179.115 -- 129.153.127.199 -- 170.75.60.8 -- 73.190.50.43 -- 23.205.205.49 -- 72.59.156.22 -- 216.180.5.197 -- 73.228.43.163 -- 151.181.7.11 -- 172.125.102.22 -- 205.141.152.140 -- 131.238.84.209 -- 150.243.205.51 -- 76.34.82.128 -- 192.243.49.78 -- 216.187.49.42 -- 52.119.179.227 -- 73.11.155.52 -- 12.24.226.11 -- 66.160.199.112 -- 205.125.1.59 -- 71.234.19.49 -- 174.205.143.5 -- 76.4.149.50 -- 66.220.115.8 -- 216.16.215.170 -- 74.72.231.72 -- 76.76.94.108 -- 71.142.232.72 -- 34.201.101.66 -- 73.243.244.217 -- 12.219.128.50 -- 67.50.205.148 -- 137.238.98.58 -- 67.255.32.238 -- 137.216.249.79 -- 52.70.254.16 -- 64.24.120.209 -- 134.122.123.198 -- 65.206.156.143 -- 35.20.108.183 -- 71.135.88.108 -- 151.141.171.14 -- 71.81.254.8 -- 72.235.216.229 -- 108.191.182.101 -- 3.21.98.173 -- 69.77.144.57 -- 24.178.114.244 -- 96.19.53.253 -- 165.124.70.127 -- 72.230.97.225 -- 24.179.7.163 -- 50.75.148.51 -- 65.185.104.117 -- 23.237.40.187 -- 209.240.80.16 -- 32.212.5.190 -- 192.24.244.68 -- 69.135.27.123 -- 17.206.214.66 -- 20.51.73.93 -- 73.187.111.156 -- 149.18.228.228 -- 75.191.104.229 -- 71.136.12.38 -- 107.21.229.156 -- 174.146.107.5 -- 98.122.123.151 -- 75.177.185.100 -- 107.51.97.245 -- 146.57.53.60 -- 173.169.100.68 -- 98.114.42.148 -- 73.181.98.22 -- 99.24.5.241 -- 174.250.93.118 -- 24.172.202.200 -- 199.16.135.10 -- 174.77.186.12 -- 67.11.20.3 -- 17.37.198.43 -- 173.223.108.171 -- 174.62.111.175 -- 208.40.172.19 -- 108.99.123.242 -- 209.112.181.234 -- 64.187.240.234 -- 13.59.70.244 -- 154.217.232.196 -- 64.139.121.142 -- 199.180.72.215 -- 50.213.112.141 -- 71.72.248.162 -- 97.115.206.218 -- 108.54.50.153 -- 34.192.62.123 -- 147.49.183.223 -- 98.153.68.218 -- 161.11.136.134 -- 108.238.9.150 -- 74.129.236.101 -- 161.242.26.154 -- 166.228.253.126 -- 139.70.20.68 -- 165.138.109.199 -- 185.171.252.177 -- 72.181.164.122 -- 47.214.158.113 -- 209.143.91.192 -- 99.28.117.134 -- 47.216.182.179 -- 13.78.188.42 -- 24.223.120.82 -- 216.41.204.141 -- 23.120.103.86 -- 96.42.64.20 -- 72.133.38.57 -- 196.57.179.208 -- 98.184.120.46 -- 47.133.136.152 -- 107.200.235.96 -- 70.173.201.114 -- 45.51.44.92 -- 137.125.66.130 -- 159.121.170.75 -- 100.31.118.108 -- 73.72.28.54 -- 64.203.249.241 -- 8.6.125.103 -- 155.206.33.128 -- 34.199.50.223 -- 172.1.8.134 -- 74.190.226.49 -- 205.170.253.139 -- 24.241.153.165 -- 99.32.76.24 -- 76.175.249.118 -- 207.171.253.134 -- 118.188.234.138 -- 68.101.181.3 -- 71.34.108.103 -- 98.253.36.160 -- 107.84.91.110 -- 68.226.146.134 -- 173.151.151.36 -- 64.56.127.60 -- 99.34.146.58 -- 23.132.40.96 -- 162.96.35.132 -- 184.236.245.246 -- 64.56.30.179 -- 139.144.43.130 -- 173.150.147.5 -- 174.61.86.228 -- 130.111.18.131 -- 44.198.68.91 -- 76.211.160.94 -- 73.75.38.196 -- 3.132.1.120 -- 173.198.144.41 -- 44.203.1.74 -- 144.90.63.107 -- 23.126.100.108 -- 73.39.7.13 -- 35.164.252.167 -- 163.150.94.117 -- 3.138.137.197 -- 208.115.63.239 -- 69.91.243.135 -- 76.120.105.6 -- 168.9.57.92 -- 132.36.207.233 -- 72.61.74.18 -- 74.191.144.230 -- 192.170.130.171 -- 52.27.166.16 -- 75.176.12.94 -- 207.191.195.194 -- 34.117.86.23 -- 209.63.231.84 -- 3.142.85.61 -- 139.171.129.30 -- 104.236.231.61 -- 209.34.138.95 -- 4.79.140.176 -- 98.225.225.102 -- 72.212.96.21 -- 54.187.137.12 -- 24.2.85.19 -- 54.200.120.106 -- 98.220.7.208 -- 136.176.57.58 -- 143.132.246.51 -- 96.40.14.21 -- 52.4.165.166 -- 104.245.32.190 -- 208.79.165.250 -- 52.73.28.144 -- 107.217.8.113 -- 20.122.200.247 -- 97.93.101.24 -- 141.153.151.7 -- 66.57.67.206 -- 67.108.254.86 -- 76.212.150.178 -- 108.235.59.66 -- 70.59.144.206 -- 66.165.20.120 -- 47.20.239.72 -- 72.202.162.117 -- 52.185.2.21 -- 50.197.56.138 -- 75.112.167.42 -- 71.87.22.216 -- 154.195.35.28 -- 74.126.65.205 -- 70.225.230.54 -- 75.184.53.43 -- 94.156.62.170 -- 161.57.52.166 -- 68.197.227.107 -- 162.39.20.162 -- 173.100.100.121 -- 72.238.194.62 -- 184.100.204.9 -- 50.43.21.247 -- 108.48.94.68 -- 74.232.42.233 -- 72.227.75.118 -- 35.233.128.200 -- 174.84.6.124 -- 4.59.24.72 -- 18.1.206.61 -- 174.152.91.194 -- 72.185.228.204 -- 108.98.207.87 -- 213.176.69.253 -- 73.175.73.164 -- 73.165.89.23 -- 216.105.227.43 -- 168.212.97.136 -- 129.7.178.194 -- 174.255.153.96 -- 184.245.57.31 -- 70.112.13.57 -- 149.76.73.134 -- 69.121.14.92 -- 208.116.202.118 -- 17.234.17.88 -- 72.85.46.6 -- 173.136.26.30 -- 50.37.125.77 -- 173.6.238.13 -- 17.198.141.72 -- 136.242.15.120 -- 12.11.109.222 -- 131.96.61.193 -- 35.226.147.204 -- 73.227.37.42 -- 170.29.68.2 -- 108.228.131.2 -- 184.210.95.116 -- 99.175.79.161 -- 24.123.157.204 -- 206.110.138.71 -- 74.98.211.72 -- 99.83.43.168 -- 67.151.244.246 -- 68.225.183.192 -- 73.188.143.115 -- 167.242.236.177 -- 71.242.253.66 -- 98.77.166.115 -- 104.231.88.76 -- 108.17.150.141 -- 167.193.70.123 -- 45.146.192.1 -- 20.114.168.16 -- 165.68.169.8 -- 67.210.158.196 -- 144.96.159.163 -- 75.106.179.98 -- 108.219.61.188 -- 75.11.17.170 -- 40.117.187.243 -- 98.225.24.13 -- 173.215.42.209 -- 96.86.11.252 -- 34.168.65.207 -- 47.27.157.126 -- 146.115.213.46 -- 198.239.226.202 -- 35.238.243.104 -- 34.133.94.3 -- 96.94.196.135 -- 173.8.173.1 -- 69.221.126.65 -- 23.31.140.180 -- 65.54.13.235 -- 68.72.199.54 -- 99.14.164.190 -- 20.88.58.30 -- 151.124.231.33 -- 47.19.34.105 -- 75.145.54.63 -- 98.214.48.236 -- 35.131.235.155 -- 69.92.215.46 -- 184.233.3.51 -- 171.65.110.108 -- 141.152.51.139 -- 24.153.113.70 -- 64.255.29.132 -- 74.103.82.42 -- 12.49.164.222 -- 134.39.55.156 -- 162.203.14.97 -- 98.195.115.94 -- 208.47.40.78 -- 20.110.57.41 -- 184.17.21.98 -- 71.176.96.120 -- 143.132.0.229 -- 67.162.170.114 -- 158.135.35.10 -- 3.87.72.143 -- 184.51.217.230 -- 108.193.75.248 -- 67.40.153.2 -- 67.83.171.37 -- 138.43.189.28 -- 76.173.211.51 -- 40.131.77.76 -- 4.31.133.167 -- 98.143.233.66 -- 96.92.63.160 -- 73.221.39.84 -- 128.46.20.106 -- 174.154.233.61 -- 107.115.250.199 -- 104.218.180.182 -- 35.148.161.247 -- 202.5.31.167 -- 20.88.156.82 -- 128.194.72.171 -- 34.224.16.0 -- 63.121.70.165 -- 12.165.64.36 -- 18.224.23.173 -- 12.174.51.207 -- 216.46.198.183 -- 71.199.121.123 -- 12.17.196.39 -- 74.112.213.141 -- 76.78.137.134 -- 12.248.231.244 -- 63.241.125.135 -- 174.26.117.151 -- 70.12.131.252 -- 40.83.228.44 -- 72.182.170.115 -- 216.211.148.2 -- 54.152.237.210 -- 74.140.220.107 -- 74.103.254.123 -- 24.170.55.19 -- 72.223.86.211 -- 50.203.198.242 -- 98.174.174.66 -- 54.244.190.18 -- 66.248.215.16 -- 174.103.233.77 -- 96.88.142.242 -- 47.248.98.23 -- 184.5.145.44 -- 65.209.10.90 -- 45.85.3.189 -- 156.68.192.190 -- 64.3.255.220 -- 156.68.10.131 -- 104.34.254.212 -- 68.45.13.100 -- 32.143.192.238 -- 206.72.85.62 -- 76.191.238.84 -- 69.40.121.45 -- 76.205.8.23 -- 54.81.111.78 -- 172.4.251.92 -- 76.214.153.185 -- 23.243.249.110 -- 137.150.223.31 -- 8.224.0.43 -- 134.71.169.201 -- 20.158.50.212 -- 128.82.27.177 -- 97.100.37.176 -- 3.30.28.132 -- 147.136.117.234 -- 98.232.182.11 -- 153.9.213.128 -- 129.237.125.143 -- 69.193.24.49 -- 143.207.30.123 -- 34.75.166.182 -- 128.171.212.246 -- 24.158.33.144 -- 198.189.235.1 -- 140.219.3.232 -- 73.91.182.95 -- 70.210.128.104 -- 171.65.10.31 -- 98.234.144.84 -- 40.79.225.75 -- 76.235.90.196 -- 173.167.182.27 -- 166.184.29.24 -- 66.64.83.9 -- 99.77.115.172 -- 35.194.49.58 -- 17.105.113.0 -- 67.245.221.120 -- 174.56.143.59 -- 96.58.181.21 -- 216.38.200.235 -- 69.131.121.15 -- 24.60.190.32 -- 23.218.194.3 -- 43.93.228.138 -- 73.176.151.218 -- 52.177.222.183 -- 76.251.87.124 -- 12.16.8.211 -- 47.19.31.192 -- 69.85.59.253 -- 199.89.222.199 -- 73.137.131.36 -- 35.233.211.47 -- 216.177.231.224 -- 148.84.208.210 -- 216.2.148.97 -- 12.217.198.135 -- 132.241.131.42 -- 104.12.121.185 -- 98.204.42.49 -- 73.160.179.66 -- 69.104.55.122 -- 40.122.138.179 -- 99.60.132.207 -- 75.118.171.240 -- 208.50.2.16 -- 75.86.94.234 -- 130.108.126.134 -- 206.124.146.18 -- 209.234.115.43 -- 140.106.170.109 -- 76.82.236.179 -- 17.18.138.207 -- 76.240.36.119 -- 23.123.21.60 -- 67.136.87.12 -- 71.168.51.208 -- 99.36.254.75 -- 209.175.15.144 -- 131.253.121.28 -- 23.116.211.63 -- 69.172.229.93 -- 96.247.137.131 -- 98.139.180.45 -- 75.198.150.145 -- 184.184.27.108 -- 146.245.120.110 -- 129.144.35.197 -- 184.217.56.201 -- 64.151.39.85 -- 70.41.139.229 -- 184.169.212.0 -- 108.202.209.179 -- 99.114.99.124 -- 17.194.175.49 -- 72.69.175.20 -- 97.101.253.129 -- 108.67.13.242 -- 98.229.237.89 -- 71.173.206.115 -- 100.26.20.99 -- 140.198.38.82 -- 149.72.11.120 -- 35.171.84.254 -- 64.250.65.129 -- 52.88.226.159 -- 99.47.58.161 -- 89.187.182.217 -- 12.11.97.200 -- 188.42.9.216 -- 68.190.164.183 -- 104.178.72.225 -- 3.85.123.126 -- 136.50.166.106 -- 24.20.47.253 -- 68.244.37.31 -- 45.37.252.195 -- 74.10.77.3 -- 169.241.215.225 -- 98.203.194.237 -- 64.186.124.127 -- 45.205.105.155 -- 173.137.187.88 -- 96.224.224.252 -- 192.187.70.209 -- 12.180.105.229 -- 143.78.73.193 -- 76.72.231.239 -- 47.6.105.227 -- 162.192.20.77 -- 70.121.45.148 -- 18.9.62.235 -- 17.27.126.53 -- 66.44.248.230 -- 216.168.91.114 -- 24.148.62.46 -- 184.224.106.35 -- 99.77.124.237 -- 68.84.19.141 -- 107.218.172.248 -- 140.160.23.121 -- 107.34.133.65 -- 104.235.250.117 -- 18.8.205.116 -- 173.114.68.35 -- 74.215.238.9 -- 45.30.255.76 -- 34.66.31.116 -- 50.22.101.97 -- 64.216.214.150 -- 136.49.51.32 -- 161.21.153.164 -- 44.195.95.210 -- 205.221.129.241 -- 204.113.215.167 -- 70.36.247.92 -- 174.205.106.210 -- 52.7.246.46 -- 146.186.83.9 -- 139.182.154.208 -- 72.20.73.130 -- 3.13.68.126 -- 138.29.243.207 -- 184.251.218.242 -- 24.21.81.156 -- 50.110.110.208 -- 73.15.146.5 -- 73.217.60.123 -- 174.128.115.41 -- 171.66.229.180 -- 108.195.235.139 -- 38.39.189.222 -- 108.202.93.193 -- 24.2.249.33 -- 54.176.192.191 -- 73.53.77.166 -- 74.109.11.120 -- 206.224.240.157 -- 52.238.88.2 -- 74.208.110.66 -- 168.65.236.108 -- 174.45.137.74 -- 73.180.92.36 -- 98.252.137.248 -- 174.230.25.112 -- 137.113.188.36 -- 198.147.21.95 -- 76.150.142.126 -- 52.190.23.109 -- 75.142.173.159 -- 50.113.19.121 -- 44.219.114.237 -- 50.235.221.97 -- 69.20.137.203 -- 20.98.222.159 -- 73.243.121.92 -- 174.61.54.203 -- 108.248.222.246 -- 52.159.190.40 -- 216.143.23.150 -- 108.185.62.119 -- 152.4.102.10 -- 54.160.101.61 -- 98.177.183.177 -- 108.35.147.217 -- 174.248.150.77 -- 47.189.94.93 -- 64.227.29.180 -- 66.9.81.40 -- 199.46.120.235 -- 72.105.161.78 -- 12.252.134.151 -- 54.173.105.181 -- 100.40.63.76 -- 172.85.45.145 -- 65.103.158.135 -- 143.110.193.18 -- 165.225.62.176 -- 70.14.77.223 -- 205.125.132.212 -- 162.238.16.190 -- 209.129.205.146 -- 66.208.160.162 -- 66.214.43.98 -- 76.30.6.170 -- 65.49.167.152 -- 198.8.0.56 -- 146.71.20.192 -- 50.49.228.159 -- 168.105.14.117 -- 149.88.111.187 -- 71.61.97.252 -- 69.89.199.13 -- 97.112.245.122 -- 71.187.95.202 -- 17.150.223.119 -- 188.241.203.239 -- 174.145.208.126 -- 104.219.115.36 -- 38.149.174.215 -- 69.110.152.7 -- 3.145.61.85 -- 67.131.173.75 -- 104.52.241.221 -- 140.198.42.117 -- 64.75.73.56 -- 71.167.132.136 -- 4.59.171.151 -- 50.46.99.218 -- 136.41.159.18 -- 44.229.242.64 -- 55.70.23.71 -- 73.237.205.143 -- 54.177.248.209 -- 173.16.197.84 -- 169.204.93.32 -- 108.59.5.220 -- 209.183.186.179 -- 129.29.176.211 -- 68.33.129.143 -- 65.101.49.140 -- 98.165.193.175 -- 98.167.229.224 -- 199.144.92.45 -- 76.91.110.88 -- 162.17.202.71 -- 74.128.220.242 -- 44.212.60.150 -- 191.237.109.149 -- 173.147.243.211 -- 108.184.247.114 -- 12.208.206.55 -- 173.113.137.82 -- 98.185.74.4 -- 52.252.174.60 -- 18.252.66.249 -- 136.55.92.116 -- 12.216.214.209 -- 67.203.54.131 -- 70.185.181.254 -- 64.146.144.148 -- 158.123.139.143 -- 69.251.168.18 -- 149.40.227.99 -- 66.115.158.170 -- 206.205.5.202 -- 24.91.100.102 -- 104.4.64.52 -- 150.160.85.82 -- 73.176.241.63 -- 99.26.175.112 -- 76.49.42.163 -- 184.196.142.70 -- 68.188.177.233 -- 71.250.213.27 -- 99.30.22.118 -- 69.71.20.175 -- 209.232.110.166 -- 98.244.75.244 -- 128.244.90.143 -- 34.133.2.57 -- 40.129.69.12 -- 160.131.168.136 -- 108.220.84.161 -- 146.82.41.14 -- 104.13.213.49 -- 68.247.109.114 -- 20.72.102.56 -- 65.249.160.205 -- 204.228.232.81 -- 98.156.52.160 -- 168.156.110.179 -- 54.188.135.9 -- 99.30.238.113 -- 174.103.199.187 -- 173.110.68.140 -- 72.104.24.214 -- 20.169.59.120 -- 24.211.117.72 -- 70.117.12.188 -- 50.217.10.154 -- 3.86.208.137 -- 174.77.25.31 -- 129.133.248.204 -- 172.251.224.204 -- 108.22.220.70 -- 47.133.98.139 -- 170.10.87.220 -- 70.238.127.196 -- 172.249.182.118 -- 209.136.161.228 -- 47.157.109.229 -- 63.144.174.130 -- 67.197.117.68 -- 34.227.162.36 -- 63.232.46.20 -- 54.158.90.114 -- 71.242.86.45 -- 73.199.166.55 -- 108.8.80.164 -- 35.208.240.205 -- 216.111.192.48 -- 98.233.205.194 -- 204.113.237.142 -- 76.181.239.150 -- 150.125.94.43 -- 216.243.49.247 -- 73.131.226.140 -- 47.133.231.108 -- 196.247.88.167 -- 34.138.36.108 -- 34.111.93.212 -- 73.117.109.38 -- 143.71.92.139 -- 75.135.179.212 -- 71.225.93.88 -- 216.54.170.137 -- 98.46.162.99 -- 75.100.84.248 -- 73.82.70.9 -- 166.170.58.22 -- 104.177.39.55 -- 32.215.138.174 -- 45.52.224.109 -- 67.228.16.150 -- 12.250.212.193 -- 50.22.113.212 -- 73.225.218.109 -- 100.25.175.20 -- 184.255.192.9 -- 205.119.95.61 -- 72.186.229.195 -- 167.73.130.130 -- 65.25.177.247 -- 99.99.170.223 -- 12.44.67.66 -- 128.2.82.73 -- 198.74.5.189 -- 52.142.59.237 -- 107.87.2.94 -- 52.45.209.253 -- 134.159.51.236 -- 18.211.8.220 -- 134.39.137.160 -- 67.173.18.166 -- 50.218.126.43 -- 173.197.83.151 -- 38.129.119.67 -- 154.27.126.64 -- 151.159.174.222 -- 99.27.130.152 -- 207.63.135.135 -- 68.32.126.195 -- 152.138.233.64 -- 12.156.124.166 -- 98.214.103.55 -- 34.233.64.8 -- 98.39.61.181 -- 128.119.153.121 -- 74.93.88.20 -- 75.172.110.29 -- 75.145.199.39 -- 97.88.110.231 -- 71.226.20.120 -- 104.39.73.66 -- 170.68.246.213 -- 208.87.77.36 -- 161.130.12.191 -- 139.127.146.127 -- 35.168.138.192 -- 8.37.86.141 -- 209.181.57.161 -- 73.173.253.220 -- 23.121.171.232 -- 72.180.94.120 -- 129.15.0.33 -- 35.94.214.132 -- 64.27.240.131 -- 12.42.21.70 -- 152.195.75.225 -- 12.193.92.175 -- 164.92.84.166 -- 172.12.156.12 -- 17.37.194.31 -- 173.171.132.121 -- 73.57.101.131 -- 65.163.68.182 -- 68.189.97.62 -- 151.213.208.30 -- 97.102.187.126 -- 70.37.217.97 -- 134.68.14.163 -- 76.167.202.15 -- 141.210.143.169 -- 128.180.76.96 -- 172.102.207.247 -- 205.177.115.224 -- 207.225.20.57 -- 34.199.228.168 -- 146.6.44.172 -- 198.189.76.30 -- 12.52.107.158 -- 128.249.200.68 -- 66.91.237.61 -- 204.102.0.191 -- 173.3.10.89 -- 69.193.131.175 -- 67.228.50.65 -- 216.134.250.171 -- 63.231.183.154 -- 69.15.83.126 -- 98.38.235.4 -- 98.5.183.88 -- 141.233.87.132 -- 70.198.61.212 -- 199.76.77.242 -- 74.214.234.101 -- 192.126.81.208 -- 100.35.220.227 -- 143.66.91.131 -- 72.63.82.216 -- 52.90.238.139 -- 164.64.191.41 -- 73.133.39.22 -- 64.131.73.167 -- 4.59.61.246 -- 71.9.57.26 -- 34.160.248.130 -- 75.91.202.29 -- 100.2.102.234 -- 98.193.71.103 -- 208.84.8.79 -- 173.141.87.23 -- 17.203.66.246 -- 45.53.30.19 -- 155.97.211.109 -- 75.134.213.116 -- 99.137.100.74 -- 146.168.50.134 -- 52.71.170.33 -- 65.79.117.11 -- 75.185.239.51 -- 173.21.174.29 -- 72.86.161.211 -- 216.131.26.99 -- 54.84.174.85 -- 173.171.106.190 -- 17.19.128.8 -- 24.43.16.151 -- 66.21.42.146 -- 12.24.221.52 -- 162.200.198.114 -- 73.42.56.129 -- 97.106.247.228 -- 20.152.7.150 -- 40.88.156.246 -- 97.90.80.111 -- 198.210.104.17 -- 3.21.64.56 -- 154.217.217.215 -- 163.238.118.169 -- 12.176.87.218 -- 174.65.248.192 -- 52.89.135.10 -- 98.34.206.165 -- 134.192.119.135 -- 45.159.195.217 -- 18.28.192.255 -- 107.132.167.139 -- 162.127.210.108 -- 216.230.119.183 -- 74.138.102.192 -- 75.51.198.121 -- 108.30.93.148 -- 71.159.59.246 -- 66.215.245.207 -- 216.32.5.66 -- 12.46.47.86 -- 23.59.124.7 -- 47.134.16.13 -- 162.201.170.50 -- 73.43.129.66 -- 108.16.86.66 -- 67.78.148.56 -- 99.57.122.148 -- 162.223.84.222 -- 23.126.223.118 -- 72.73.202.54 -- 67.244.6.238 -- 73.211.110.151 -- 50.110.221.22 -- 67.167.237.155 -- 198.241.4.191 -- 76.251.10.209 -- 38.121.202.27 -- 173.66.245.103 -- 47.195.15.5 -- 134.202.136.241 -- 72.218.25.112 -- 66.155.89.180 -- 108.81.181.228 -- 76.186.202.45 -- 35.144.5.245 -- 108.36.172.89 -- 3.88.167.179 -- 129.33.3.41 -- 24.46.187.70 -- 75.109.149.134 -- 163.120.2.239 -- 75.142.132.26 -- 173.167.146.45 -- 70.129.242.70 -- 40.103.49.58 -- 216.164.208.170 -- 72.253.156.1 -- 47.153.16.71 -- 34.135.207.24 -- 184.177.100.216 -- 12.46.190.193 -- 217.148.141.154 -- 69.8.148.192 -- 97.119.106.184 -- 52.23.254.167 -- 73.4.245.30 -- 70.7.128.241 -- 71.13.199.169 -- 173.130.231.220 -- 18.13.80.103 -- 64.124.79.235 -- 24.210.181.118 -- 50.22.33.22 -- 132.163.201.137 -- 76.214.232.252 -- 73.86.253.92 -- 134.74.168.32 -- 65.154.191.19 -- 35.186.73.17 -- 204.134.244.155 -- 198.212.12.252 -- 75.130.250.91 -- 47.205.219.173 -- 24.145.8.227 -- 159.250.213.195 -- 50.112.29.66 -- 143.236.151.28 -- 64.157.244.101 -- 73.150.99.118 -- 165.95.114.44 -- 75.40.158.41 -- 216.161.118.38 -- 67.141.139.30 -- 68.180.206.51 -- 70.3.189.220 -- 150.212.106.241 -- 70.3.116.136 -- 45.26.178.202 -- 99.60.25.118 -- 71.246.231.234 -- 12.172.0.236 -- 24.172.207.167 -- 66.23.205.167 -- 190.92.143.116 -- 96.17.208.173 -- 72.182.168.156 -- 76.103.43.239 -- 47.136.81.133 -- 73.119.107.57 -- 34.82.247.225 -- 128.208.37.189 -- 216.106.125.236 -- 66.190.201.108 -- 164.92.21.175 -- 17.200.192.15 -- 130.191.49.251 -- 54.234.226.163 -- 128.171.71.41 -- 184.18.30.151 -- 104.98.79.218 -- 198.16.52.68 -- 199.6.40.151 -- 69.62.70.188 -- 71.6.215.163 -- 47.134.20.106 -- 76.198.183.10 -- 209.155.176.13 -- 64.198.213.19 -- 74.255.252.234 -- 128.111.88.33 -- 73.80.148.122 -- 73.9.72.2 -- 17.4.141.233 -- 107.185.138.57 -- 104.39.151.88 -- 208.73.75.171 -- 47.188.40.8 -- 12.147.100.153 -- 35.212.88.141 -- 24.107.123.13 -- 40.129.49.165 -- 137.83.107.192 -- 38.96.0.174 -- 68.51.156.5 -- 99.200.30.15 -- 73.11.5.179 -- 140.251.234.149 -- 209.33.46.178 -- 8.14.107.71 -- 12.20.53.195 -- 157.56.189.251 -- 73.200.166.79 -- 65.198.231.12 -- 216.206.154.93 -- 204.15.30.9 -- 107.201.77.211 -- 8.42.142.103 -- 107.27.52.39 -- 50.220.91.83