diff --git a/app/controllers/clients_controller.rb b/app/controllers/clients_controller.rb index 4d2803c06..4cd8e7ea9 100644 --- a/app/controllers/clients_controller.rb +++ b/app/controllers/clients_controller.rb @@ -23,7 +23,8 @@ def index elsif params[:ids].present? response = Client.find_by_id(params[:ids], page: page, sort: sort) else - response = Client.query(params[:query], + response = Client.query( + params[:query], year: params[:year], from_date: params[:from_date], until_date: params[:until_date], @@ -33,9 +34,10 @@ def index software: params[:software], certificate: params[:certificate], repository_type: params[:repository_type], - client_type: params[:client_type], - page: page, - sort: sort) + client_type: params[:client_type], + page: page, + sort: sort, + ) end begin @@ -123,11 +125,18 @@ def create end def update - if @client.update(safe_params) - options = {} - options[:is_collection] = false - options[:params] = { current_ability: current_ability } - + options = {} + options[:is_collection] = false + options[:params] = { current_ability: current_ability } + + if params.dig(:data, :attributes, :mode) == "transfer" + # only update provider_id + authorize! :transfer, @client + + @client.transfer(safe_params.slice(:target_id)) + render json: ClientSerializer.new(@client, options).serialized_json, status: :ok + elsif @client.update(safe_params) + render json: ClientSerializer.new(@client, options).serialized_json, status: :ok else Rails.logger.error @client.errors.inspect diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index acd15141d..e7c5fa38f 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -155,10 +155,17 @@ def create end def update - if @client.update_attributes(safe_params) - options = {} - options[:is_collection] = false - options[:params] = { current_ability: current_ability } + options = {} + options[:is_collection] = false + options[:params] = { current_ability: current_ability } + + if params.dig(:data, :attributes, :mode) == "transfer" + # only update provider_id + authorize! :transfer, @client + + @client.transfer(safe_params.slice(:target_id)) + render json: RepositorySerializer.new(@client, options).serialized_json, status: :ok + elsif @client.update(safe_params) render json: RepositorySerializer.new(@client, options).serialized_json, status: :ok else diff --git a/app/jobs/transfer_client_job.rb b/app/jobs/transfer_client_job.rb new file mode 100644 index 000000000..f0a7c35fd --- /dev/null +++ b/app/jobs/transfer_client_job.rb @@ -0,0 +1,22 @@ +class TransferClientJob < ActiveJob::Base + queue_as :lupo_background + + def perform(client, options = {}) + symbol = client.symbol + + if client.present? && options[:target_id].present? + options = { + filter: { client_id: symbol.downcase }, + label: "[ClientTransfer]", + job_name: "UpdateProviderIdJob", + target_id: options[:target_id], + } + + Doi.loop_through_dois(options) + + Rails.logger.info "[Transfer] DOIs updating has started for #{symbol} to #{options[:target_id]}." + else + Rails.logger.error "[Transfer] Error updating DOIs " + symbol + ": not found" + end + end +end diff --git a/app/jobs/update_provider_id_job.rb b/app/jobs/update_provider_id_job.rb new file mode 100644 index 000000000..50ceed98f --- /dev/null +++ b/app/jobs/update_provider_id_job.rb @@ -0,0 +1,22 @@ +class UpdateProviderIdJob < ActiveJob::Base + queue_as :lupo_transfer + + # retry_on ActiveRecord::RecordNotFound, wait: 10.seconds, attempts: 3 + # retry_on Faraday::TimeoutError, wait: 10.minutes, attempts: 3 + + # discard_on ActiveJob::DeserializationError + + def perform(doi_id, options = {}) + doi = Doi.where(doi: doi_id).first + + if doi.present? && options[:target_id].present? + doi.__elasticsearch__.index_document + + Rails.logger.info "[Transfer] updated DOI #{doi.doi}." + elsif doi.present? + Rails.logger.error "[Transfer] Error updateding DOI " + doi_id + ": no target client" + else + Rails.logger.error "[Transfer] Error updateding DOI " + doi_id + ": not found" + end + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index c7d33f631..bb6bab8fe 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -30,6 +30,7 @@ def initialize(user) can [:manage], Client do |client| client.provider && user.provider_id.casecmp(client.provider.consortium_id) end + cannot [:transfer], Client can [:manage], ClientPrefix #, :client_id => user.provider_id # if Flipper[:delete_doi].enabled?(user) @@ -51,6 +52,7 @@ def initialize(user) can [:update, :read, :read_billing_information], Provider, symbol: user.provider_id.upcase can [:manage], ProviderPrefix, provider_id: user.provider_id can [:manage], Client, provider_id: user.provider_id + cannot [:transfer], Client can [:manage], ClientPrefix #, :client_id => user.provider_id # if Flipper[:delete_doi].enabled?(user) diff --git a/app/models/client.rb b/app/models/client.rb index 3d75e5914..7eb53df05 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -327,6 +327,55 @@ def target_id=(value) Doi.transfer(client_id: symbol.downcase, target_id: target.id) end + def transfer(options = {}) + if options[:target_id].blank? + Rails.logger.error "[Transfer] No target provider provided." + return nil + end + + target_provider = Provider.where(symbol: options[:target_id]).first + + if target_provider.blank? + Rails.logger.error "[Transfer] Provider doesn't exist." + return nil + end + + unless ["direct_member", "consortium_organization"].include?(target_provider.member_type) + Rails.logger.error "[Transfer] Consortiums and Members-only cannot have repositories." + return nil + end + + # Transfer client + update_attribute(:allocator, target_provider.id) + + # transfer prefixes + transfer_prefixes(target_provider.symbol) + + # Update DOIs + TransferClientJob.perform_later(self, target_id: options[:target_id]) + end + + def transfer_prefixes(target_id) + # These prefixes are used by multiple clients + prefixes_to_keep = ["10.4124", "10.4225", "10.4226", "10.4227"] + + # delete all associated prefixes + associated_prefixes = prefixes.reject{ |prefix| prefixes_to_keep.include?(prefix.uid)} + prefix_ids = associated_prefixes.pluck(:id) + prefixes_names = associated_prefixes.pluck(:uid) + + if prefix_ids.present? + response = ProviderPrefix.where("prefix_id IN (?)", prefix_ids).destroy_all + puts "#{response.count} provider prefixes deleted." + end + + # Assign prefix(es) to provider + prefixes_names.each do |prefix| + ProviderPrefix.create(provider_id: target_id, prefix_id: prefix) + puts "Provider prefix for provider #{target_id} and prefix #{prefix} created." + end + end + def service_contact_email service_contact.fetch("email",nil) if service_contact.present? end diff --git a/app/models/concerns/authenticable.rb b/app/models/concerns/authenticable.rb index 8296ecb4f..49cf9ec02 100644 --- a/app/models/concerns/authenticable.rb +++ b/app/models/concerns/authenticable.rb @@ -159,7 +159,7 @@ def get_payload(uid: nil, user: nil, password: nil) # we only need password for clients registering DOIs in the handle system if uid.include? "." payload.merge!( - "provider_id" => uid.split(".", 2).first, + "provider_id" => user.provider_id, "client_id" => uid, "password" => password, ) @@ -298,15 +298,15 @@ def get_payload(uid: nil, user: nil, password: nil) # we only need password for clients registering DOIs in the handle system if uid.include? "." - payload.merge!({ - "provider_id" => uid.split(".", 2).first, + payload.merge!( + "provider_id" => user.provider_id, "client_id" => uid, - "password" => password - }) + "password" => password, + ) elsif uid != "admin" - payload.merge!({ + payload.merge!( "provider_id" => uid - }) + ) end payload diff --git a/app/models/doi.rb b/app/models/doi.rb index 279906536..e0a0b7d5d 100644 --- a/app/models/doi.rb +++ b/app/models/doi.rb @@ -1780,7 +1780,7 @@ def self.transfer(options={}) # +job_name+:: Acive Job class name of the Job that would be executed on every matched results def self.loop_through_dois(options) size = (options[:size] || 1000).to_i - cursor = [options[:from_id], options[:until_id]] + cursor = [options[:from_id] || Doi.minimum(:id).to_i, options[:until_id] || Doi.maximum(:id).to_i] filter = options[:filter] || {} label = options[:label] || "" job_name = options[:job_name] || "" @@ -1802,7 +1802,7 @@ def self.loop_through_dois(options) ids = response.results.results.map(&:uid) ids.each do |id| - Object.const_get(job_name).perform_later(id, filter) + Object.const_get(job_name).perform_later(id, options) end end end diff --git a/app/models/event.rb b/app/models/event.rb index 6ea95c503..c65cd4030 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -561,7 +561,7 @@ def self.label_state_event(event) # +job_name+:: Acive Job class name of the Job that would be executed on every matched results def self.loop_through_events(options) size = (options[:size] || 1000).to_i - cursor = [options[:from_id], options[:until_id]] + cursor = [options[:from_id] || Doi.minimum(:id).to_i, options[:until_id] || Doi.maximum(:id).to_i] filter = options[:filter] || {} label = options[:label] || "" job_name = options[:job_name] || "" diff --git a/db/seeds/development/base.seeds.rb b/db/seeds/development/base.seeds.rb index dd1c92b03..35ade98b8 100644 --- a/db/seeds/development/base.seeds.rb +++ b/db/seeds/development/base.seeds.rb @@ -8,7 +8,8 @@ client = Client.where(symbol: "DATACITE.TEST").first || FactoryBot.create(:client, provider: provider, symbol: ENV["MDS_USERNAME"], password: ENV["MDS_PASSWORD"]) if Prefix.where(uid: "10.14454").blank? prefix = FactoryBot.create(:prefix, uid: "10.14454") - FactoryBot.create(:client_prefix, client_id: client.id, prefix_id: prefix.id) + FactoryBot.create(:provider_prefix, provider_id: provider.symbol, prefix_id: prefix.uid) + FactoryBot.create(:client_prefix, client_id: client.symbol, prefix_id: prefix.uid) end dois = FactoryBot.create_list(:doi, 10, client: client, state: "findable") FactoryBot.create_list(:event_for_datacite_related, 3, obj_id: dois.first.doi) diff --git a/db/seeds/development/consortium_transfer.seeds.rb b/db/seeds/development/consortium_transfer.seeds.rb new file mode 100644 index 000000000..ce1ec1ad1 --- /dev/null +++ b/db/seeds/development/consortium_transfer.seeds.rb @@ -0,0 +1,17 @@ +require "factory_bot_rails" + +fail "Seed tasks can only be used in the development enviroment" if Rails.env.production? + +after "development:base" do + + provider = Provider.where(symbol: "QUECHUA").first || FactoryBot.create(:provider, symbol: "QUECHUA") + client = Client.where(symbol: "QUECHUA.TEXT").first || FactoryBot.create(:client, provider: provider, symbol: "QUECHUA.TEXT", password: ENV["MDS_PASSWORD"]) + if Prefix.where(uid: "10.14459").blank? + prefix = FactoryBot.create(:prefix, uid: "10.14459") + FactoryBot.create(:provider_prefix, provider_id: provider.symbol, prefix_id: prefix.uid) + FactoryBot.create(:client_prefix, client_id: client.symbol, prefix_id: prefix.uid) + end + dois = FactoryBot.create_list(:doi, 10, client: client, state: "findable") + FactoryBot.create_list(:event_for_datacite_related, 3, obj_id: dois.first.doi) + FactoryBot.create_list(:event_for_datacite_usage, 2, obj_id: dois.first.doi) +end diff --git a/spec/concerns/authenticable_spec.rb b/spec/concerns/authenticable_spec.rb index af60723f0..0b0ec1b36 100644 --- a/spec/concerns/authenticable_spec.rb +++ b/spec/concerns/authenticable_spec.rb @@ -286,7 +286,13 @@ describe 'decode_auth_param' do it "works" do - expect(subject.decode_auth_param(username: subject.symbol, password: 12345)).to eq("uid"=>subject.symbol.downcase, "name"=>subject.name, "email"=>subject.system_email, "password" => "12345", "role_id"=>"client_admin", "provider_id"=>subject.symbol.downcase.split(".").first, "client_id"=>subject.symbol.downcase) + expect(subject.decode_auth_param(username: subject.symbol, password: 12345)).to eq("uid"=>subject.symbol.downcase, "name"=>subject.name, "email"=>subject.system_email, "password" => "12345", "role_id"=>"client_admin", "provider_id"=>subject.provider_id, "client_id"=>subject.symbol.downcase) + end + end + + describe 'get_payload' do + it "works" do + expect(subject.get_payload(uid: subject.symbol.downcase, user: subject, password: 12345)).to eq("uid"=>subject.symbol.downcase, "name"=>subject.name, "email"=>subject.system_email, "password" => 12345, "role_id"=>"client_admin", "provider_id"=>subject.provider_id, "client_id"=>subject.symbol.downcase) end end end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 7a928d087..72d13bcce 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -38,6 +38,7 @@ it { is_expected.not_to be_able_to(:create, client) } it { is_expected.not_to be_able_to(:update, client) } it { is_expected.not_to be_able_to(:destroy, client) } + it { is_expected.not_to be_able_to(:transfer, client) } it { is_expected.not_to be_able_to(:read, prefix) } it { is_expected.not_to be_able_to(:create, prefix) } @@ -65,6 +66,7 @@ it { is_expected.not_to be_able_to(:create, client) } it { is_expected.to be_able_to(:update, client) } it { is_expected.not_to be_able_to(:destroy, client) } + it { is_expected.not_to be_able_to(:transfer, client) } it { is_expected.not_to be_able_to(:read, prefix) } it { is_expected.not_to be_able_to(:create, prefix) } @@ -97,6 +99,7 @@ it { is_expected.not_to be_able_to(:create, client) } it { is_expected.not_to be_able_to(:update, client) } it { is_expected.not_to be_able_to(:destroy, client) } + it { is_expected.not_to be_able_to(:transfer, client) } it { is_expected.not_to be_able_to(:read, prefix) } it { is_expected.not_to be_able_to(:create, prefix) } @@ -129,6 +132,7 @@ it { is_expected.to be_able_to(:create, client) } it { is_expected.to be_able_to(:update, client) } it { is_expected.to be_able_to(:destroy, client) } + it { is_expected.not_to be_able_to(:transfer, client) } it { is_expected.not_to be_able_to(:read, prefix) } it { is_expected.not_to be_able_to(:create, prefix) } @@ -161,6 +165,7 @@ it { is_expected.to be_able_to(:create, provider) } it { is_expected.to be_able_to(:update, provider) } it { is_expected.to be_able_to(:destroy, provider) } + it { is_expected.not_to be_able_to(:transfer, client) } it { is_expected.to be_able_to(:read, client) } it { is_expected.to be_able_to(:create, client) } @@ -198,6 +203,7 @@ it { is_expected.not_to be_able_to(:create, client) } it { is_expected.not_to be_able_to(:update, client) } it { is_expected.not_to be_able_to(:destroy, client) } + it { is_expected.not_to be_able_to(:transfer, client) } it { is_expected.not_to be_able_to(:read, prefix) } it { is_expected.not_to be_able_to(:create, prefix) } @@ -223,6 +229,7 @@ it { is_expected.to be_able_to(:create, provider) } it { is_expected.to be_able_to(:update, provider) } it { is_expected.to be_able_to(:destroy, provider) } + it { is_expected.to be_able_to(:transfer, client) } it { is_expected.to be_able_to(:read, client) } it { is_expected.to be_able_to(:create, client) } @@ -250,6 +257,7 @@ it { is_expected.not_to be_able_to(:create, client) } it { is_expected.not_to be_able_to(:update, client) } it { is_expected.not_to be_able_to(:destroy, client) } + it { is_expected.not_to be_able_to(:transfer, client) } it { is_expected.to be_able_to(:read, doi) } it { is_expected.not_to be_able_to(:transfer, doi) } @@ -269,6 +277,7 @@ it { is_expected.not_to be_able_to(:create, client) } it { is_expected.not_to be_able_to(:update, client) } it { is_expected.not_to be_able_to(:destroy, client) } + it { is_expected.not_to be_able_to(:transfer, client) } it { is_expected.to be_able_to(:read, doi) } it { is_expected.not_to be_able_to(:transfer, doi) } diff --git a/spec/models/client_spec.rb b/spec/models/client_spec.rb index 2805364ec..69bc481fb 100644 --- a/spec/models/client_spec.rb +++ b/spec/models/client_spec.rb @@ -23,6 +23,101 @@ end end + describe "Client transfer" do + let!(:prefixes) { create_list(:prefix, 3) } + let!(:prefix) { prefixes.first } + + let!(:client_prefix) { create(:client_prefix, client: client, prefix: prefix) } + let!(:provider_prefix) { create(:provider_prefix, provider: provider, prefix: prefix) } + let!(:provider_prefix_more) { create(:provider_prefix, provider: provider, prefix: prefixes.last) } + let(:new_provider) { create(:provider, symbol: "QUECHUA", member_type: "direct_member") } + let(:options) { { target_id: new_provider.symbol } } + let(:bad_options) { { target_id: "SALS" } } + + context "to direct_member" do + it "works" do + client.transfer(options) + + expect(client.provider_id).to eq(new_provider.symbol.downcase) + expect(new_provider.prefixes.length).to eq(1) + expect(provider.prefixes.length).to eq(1) + + expect(new_provider.prefix_ids).to include(prefix.uid) + expect(provider.prefix_ids).not_to include(prefix.uid) + end + + it "it doesn't transfer" do + client.transfer(bad_options) + + expect(client.provider_id).to eq(provider.symbol.downcase) + expect(provider.prefixes.length).to eq(2) + expect(provider.prefix_ids).to include(prefix.uid) + end + end + + context "to member_only" do + let(:new_provider) { create(:provider, symbol: "QUECHUA", member_type: "member_only") } + let(:options) { { target_id: new_provider.symbol } } + + it "it doesn't transfer" do + client.transfer(options) + + expect(client.provider_id).to eq(provider.symbol.downcase) + expect(provider.prefixes.length).to eq(2) + expect(provider.prefix_ids).to include(prefix.uid) + end + end + + context "to consortium_organization" do + let(:new_provider) { create(:provider, symbol: "QUECHUA", member_type: "consortium_organization") } + let(:options) { { target_id: new_provider.symbol } } + + it "works" do + client.transfer(options) + + expect(client.provider_id).to eq(new_provider.symbol.downcase) + expect(new_provider.prefixes.length).to eq(1) + expect(provider.prefixes.length).to eq(1) + + expect(new_provider.prefix_ids).to include(prefix.uid) + expect(provider.prefix_ids).not_to include(prefix.uid) + end + end + + context "to consortium" do + let(:new_provider) { create(:provider, symbol: "QUECHUA", role_name: "ROLE_CONSORTIUM") } + let(:options) { { target_id: new_provider.symbol } } + + it "it doesn't transfer" do + client.transfer(options) + + expect(client.provider_id).to eq(provider.symbol.downcase) + expect(provider.prefixes.length).to eq(2) + expect(provider.prefix_ids).to include(prefix.uid) + end + end + end + + describe "Client prefixes transfer" do + let!(:prefixes) { create_list(:prefix, 3) } + let!(:prefix) { prefixes.first } + let!(:client_prefix) { create(:client_prefix, client: client, prefix: prefix) } + let!(:provider_prefix) { create(:provider_prefix, provider: provider, prefix: prefix) } + let!(:provider_prefix_more) { create(:provider_prefix, provider: provider, prefix: prefixes.last) } + let(:new_provider) { create(:provider, symbol: "QUECHUA") } + + it "works" do + client.transfer_prefixes(new_provider.symbol) + + expect(new_provider.prefixes.length).to eq(1) + expect(provider.prefixes.length).to eq(1) + + expect(new_provider.prefix_ids).to include(prefix.uid) + expect(provider.prefix_ids).not_to include(prefix.uid) + end + end + + describe "methods" do it "should not update the symbol" do client.update_attributes :symbol => client.symbol+'foo.bar' diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index aecb26786..9a439f10c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -89,7 +89,7 @@ end it "has provider_id" do - expect(user.provider_id).to eq(client.symbol.downcase.split(".").first) + expect(user.provider_id).to eq(client.provider_id) end it "has client" do diff --git a/spec/requests/clients_spec.rb b/spec/requests/clients_spec.rb index 0f66312ed..154d40961 100644 --- a/spec/requests/clients_spec.rb +++ b/spec/requests/clients_spec.rb @@ -218,6 +218,47 @@ end end + context "transfer repository" do + let(:new_provider) { create(:provider, symbol: "QUECHUA", password_input: "12345") } + let!(:prefixes) { create_list(:prefix, 3) } + let!(:prefix) { prefixes.first } + let!(:client_prefix) { create(:client_prefix, client: client, prefix: prefix) } + let!(:provider_prefix) { create(:provider_prefix, provider: provider, prefix: prefix) } + let!(:provider_prefix_more) { create(:provider_prefix, provider: provider, prefix: prefixes.last) } + let(:doi) { create_list(:doi, 10, client: client) } + + + let(:params) do + { + "data" => { + "type" => "clients", + "attributes" => { + "mode" => "transfer", + "targetId" => new_provider.symbol, + }, + }, + } + end + + it "updates the record" do + put "/clients/#{client.symbol}", params, headers + + expect(last_response.status).to eq(200) + expect(json.dig("data", "attributes", "name")).to eq("My data center") + expect(json.dig("data", "relationships", "provider", "data", "id")).to eq("quechua") + expect(json.dig("data", "relationships", "prefixes", "data").first.dig("id")).to eq(prefix.uid) + + get "/providers/#{provider.symbol}" + + expect(json.dig("data", "relationships", "prefixes", "data").length).to eq(1) + expect(json.dig("data", "relationships", "prefixes", "data").first.dig("id")).to eq(prefixes.last.uid) + + get "/providers/#{new_provider.symbol}" + + expect(json.dig("data", "relationships", "prefixes", "data").first.dig("id")).to eq(prefix.uid) + end + end + context 'invalid globus_uuid' do let(:params) do { "data" => { "type" => "clients", diff --git a/spec/requests/repositories_spec.rb b/spec/requests/repositories_spec.rb index e2523ca90..07ef6031a 100644 --- a/spec/requests/repositories_spec.rb +++ b/spec/requests/repositories_spec.rb @@ -281,6 +281,46 @@ end end + context "transfer repository" do + let(:bearer) { User.generate_token } + let(:staff_headers) { {'HTTP_ACCEPT'=>'application/vnd.api+json', 'HTTP_AUTHORIZATION' => 'Bearer ' + bearer}} + + let(:new_provider) { create(:provider, symbol: "QUECHUA", password_input: "12345") } + let!(:prefix) { create(:prefix) } + let!(:client_prefix) { create(:client_prefix, client: client, prefix: prefix) } + let!(:provider_prefix) { create(:provider_prefix, provider: provider, prefix: prefix) } + let(:doi) { create_list(:doi, 10, client: client) } + + let(:params) do + { + "data" => { + "type" => "clients", + "attributes" => { + "mode" => "transfer", + "targetId" => new_provider.symbol, + }, + }, + } + end + + it "updates the record" do + put "/repositories/#{client.symbol}", params, staff_headers + + expect(last_response.status).to eq(200) + expect(json.dig("data", "attributes", "name")).to eq("My data center") + expect(json.dig("data", "relationships", "provider", "data", "id")).to eq("quechua") + expect(json.dig("data", "relationships", "prefixes", "data").first.dig("id")).to eq(prefix.uid) + + get "/providers/#{provider.symbol}" + + expect(json.dig("data", "relationships", "prefixes", "data")).to be_empty + + get "/providers/#{new_provider.symbol}" + + expect(json.dig("data", "relationships", "prefixes", "data").first.dig("id")).to eq(prefix.uid) + end + end + context 'invalid globus_uuid' do let(:params) do { "data" => { "type" => "repositories",