diff --git a/app/models/mediaflux/http/asset_approve_request.rb b/app/models/mediaflux/http/asset_approve_request.rb new file mode 100644 index 00000000..f9a8cb65 --- /dev/null +++ b/app/models/mediaflux/http/asset_approve_request.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true +module Mediaflux + module Http + # Constructs a request to mediaflux to approve a project + # + # @example + # project = Project.first + # project.save_in_mediaflux(session_id: User.first.mediaflux_session) + # approve_req = Mediaflux::Http::AssetApproveRequest.new(session_token: User.first.mediaflux_session, project:) + # approve_req.resolve + # + class AssetApproveRequest < Request + attr_reader :project_metadata, :project + # Constructor + # @param session_token [String] the API token for the authenticated session + # @param project [Project] project to approve + # @param xml_namespace [String] XML namespace for the element + def initialize(session_token:, project:, xml_namespace: nil, xml_namespace_uri: nil) + super(session_token: session_token) + @project = project + @project_metadata = project.metadata + @xml_namespace = xml_namespace || self.class.default_xml_namespace + @xml_namespace_uri = xml_namespace_uri || self.class.default_xml_namespace_uri + end + + # Specifies the Mediaflux service to use when updating assets + # @return [String] + def self.service + "asset.set" + end + + private + + # The generated XML mimics what we get when we issue an Aterm command as follows: + # service.execute :service -name "asset.set" \ + # < :id "1574" :meta -action "replace" < :tigerdata:project -xmlns:tigerdata "tigerdata" < \ + # :ProjectDirectory "/td-demo-001/tigerdataNS/test-05-30-24" :Title "testing approval" :Description "I want to test the approval updates" \ + # :Status "approved" :DataSponsor "cac9" :DataManager "mjc12" :Department "RDSS" :DataUser -ReadOnly "true" "la15" :DataUser "woongkim" \ + # :CreatedOn "30-MAY-2024 09:11:09" :CreatedBy "cac9" :ProjectID "10.34770/tbd" + # :StorageCapacity < :Size -Requested "500" -Approved "1" "1" :Unit -Requested "GB" -Approved "TB" "TB" > \ + # :Performance -Requested "Standard" -Approved "Standard" "Standard" :Submission < :RequestedBy "cac9" \ + # :RequestDateTime "30-MAY-2024 13:11:09" :ApprovedBy "cac9" :ApprovalDateTime "30-MAY-2024 13:12:44" \ + # :EventlNote < :NoteDateTime "30-MAY-2024 13:12:44" :NoteBy "cac9" :EventType "Quota" :Message "A note"\ + # > > :ProjectPurpose "Research" :SchemaVersion "0.6.1" > > > + # + def build_http_request_body(name:) + super do |xml| + xml.args do + xml.id project.mediaflux_id + xml.meta do + xml.parent.set_attribute("action", "replace") + doc = xml.doc + root = doc.root + # Define the namespace only if this is required + root.add_namespace_definition(@xml_namespace, @xml_namespace_uri) + + element_name = @xml_namespace.nil? ? "project" : "#{@xml_namespace}:project" + xml.send(element_name) do + build_project(xml) + end + end + end + end + end + + def build_project(xml) + build_basic_project_meta(xml) + build_departments(xml, project_metadata[:departments]) + build_read_only_user(xml, project_metadata[:data_user_read_only]) + build_read_write_users(xml, project_metadata[:data_user_read_write]) + xml.CreatedOn self.class.format_date_for_mediaflux(project_metadata[:created_on]) + xml.CreatedBy project_metadata[:created_by] + xml.ProjectID project_metadata[:project_id] + build_storage_capacity(xml) + build_performance(xml) + build_submission(xml) + xml.ProjectPurpose project_metadata[:project_purpose] + xml.SchemaVersion TigerdataSchema::SCHEMA_VERSION + end + + def build_basic_project_meta(xml) + xml.ProjectDirectory project_metadata[:project_directory] + xml.Title project_metadata[:title] + xml.Description project_metadata[:description] if project_metadata[:description].present? + xml.Status project_metadata[:status] + xml.DataSponsor project_metadata[:data_sponsor] + xml.DataManager project_metadata[:data_manager] + end + + def build_departments(xml, departments) + return if departments.blank? + + departments.each do |department| + xml.Department department + end + end + + def build_read_only_user(xml, ro_users) + return if ro_users.blank? + + ro_users.each do |ro_user| + xml.DataUser do + xml.parent.set_attribute("ReadOnly", true) + xml.text(ro_user) + end + end + end + + def build_read_write_users(xml, rw_users) + return if rw_users.blank? + + rw_users.each do |rw_user| + xml.DataUser rw_user + end + end + + def build_storage_capacity(xml) + xml.StorageCapacity do + xml.Size do + build_value(xml, project_metadata[:storage_capacity][:size][:requested], project_metadata[:storage_capacity][:size][:approved]) + end + xml.Unit do + build_value(xml, project_metadata[:storage_capacity][:unit][:requested], project_metadata[:storage_capacity][:unit][:approved]) + end + end + end + + def build_performance(xml) + xml.Performance do + build_value(xml, project_metadata[:storage_performance_expectations][:requested], project_metadata[:storage_performance_expectations][:approved]) + end + end + + def build_value(xml, requested, approved) + xml.parent.set_attribute("Requested", requested) + xml.parent.set_attribute("Approved", approved) + xml.text(approved) + end + + def build_submission(xml) + xml.Submission do + xml.RequestedBy submission_event.event_person + xml.RequestDateTime self.class.format_date_for_mediaflux(submission_event.created_at.iso8601) + xml.ApprovedBy approval_event.event_person + xml.ApprovalDateTime self.class.format_date_for_mediaflux(approval_event.created_at.iso8601) + build_submission_note(xml) + end + end + + def build_submission_note(xml) + return if approval_event.event_note.blank? + + xml.EventlNote do + xml.NoteDateTime self.class.format_date_for_mediaflux(approval_event.created_at.iso8601) + xml.NoteBy approval_event.event_note["note_by"] + xml.EventType approval_event.event_note["event_type"] + xml.Message approval_event.event_note["message"] + end + end + + def approval_event + @approval_event ||= project.provenance_events.find_by(event_type: ProvenanceEvent::APPROVAL_EVENT_TYPE) + end + + def submission_event + @submission_event ||= project.provenance_events.find_by(event_type: ProvenanceEvent::SUBMISSION_EVENT_TYPE) + end + end + end +end diff --git a/app/models/mediaflux/http/request.rb b/app/models/mediaflux/http/request.rb index 9aece971..f3f36d9d 100644 --- a/app/models/mediaflux/http/request.rb +++ b/app/models/mediaflux/http/request.rb @@ -142,6 +142,13 @@ def xtoshell_xml( name: self.class.service) xml.strip.gsub("\"","'").gsub("","").gsub("","") end + # This method is used for transforming iso8601 dates to dates that MediaFlux likes + # Take a string like "2024-02-26T10:33:11-05:00" and convert this string to "22-FEB-2024 13:57:19" + def self.format_date_for_mediaflux(iso8601_date) + return if iso8601_date.nil? + Time.parse(iso8601_date).strftime("%e-%b-%Y %H:%M:%S").upcase + end + private def http_request diff --git a/app/models/project_metadata.rb b/app/models/project_metadata.rb index 2ea51ae1..a298f8f3 100644 --- a/app/models/project_metadata.rb +++ b/app/models/project_metadata.rb @@ -43,11 +43,16 @@ def approve_project(params:) project.metadata_json["status"] = Project::APPROVED_STATUS project.metadata_json["project_directory"] = "#{params[:project_directory_prefix]}/#{params[:project_directory]}" project.metadata_json["storage_capacity"] = params[:storage_capacity] + project.metadata_json["storage_performance_expectations"] = params[:storage_performance_expectations] project.save! + generate_approval_events(params[:approval_note]) + end + + def generate_approval_events(note) # create two provenance events, one for approving the project and another for changing the status of the project project.provenance_events.create(event_type: ProvenanceEvent::APPROVAL_EVENT_TYPE, event_person: current_user.uid, event_details: "Approved by #{current_user.display_name_safe}", - event_note: params[:approval_note]) + event_note: note) project.provenance_events.create(event_type: ProvenanceEvent::STATUS_UPDATE_EVENT_TYPE, event_person: current_user.uid, event_details: "The Status of this project has been set to approved") end diff --git a/spec/factories/project.rb b/spec/factories/project.rb index ae14201c..2830016c 100644 --- a/spec/factories/project.rb +++ b/spec/factories/project.rb @@ -17,6 +17,8 @@ project_purpose { "research" } project_directory { "big-data" } schema_version { ::TigerdataSchema::SCHEMA_VERSION } + approved_by { nil } + approved_on { nil } end mediaflux_id { nil } metadata do @@ -38,7 +40,9 @@ storage_capacity: storage_capacity, storage_performance_expectations: storage_performance, project_purpose: project_purpose, - schema_version: schema_version + schema_version: schema_version, + approved_by: approved_by, + approved_on: approved_on } end factory :project_with_doi, class: "Project" do @@ -53,5 +57,16 @@ end end end + + factory :approved_project, class: "Project" do + transient do + storage_capacity { { size: { requested: 500, approved: 600 }, unit: { requested: "GB", approved: "KB" } } } + storage_performance { { requested: "standard", approved: "performant" } } + status { "approved" } + approved_by { FactoryBot.create(:sysadmin).uid } + approved_on { Time.current.in_time_zone("America/New_York").iso8601 } + project_id { "10.34770/tbd" } + end + end end end diff --git a/spec/models/mediaflux/http/asset_approve_request_spec.rb b/spec/models/mediaflux/http/asset_approve_request_spec.rb new file mode 100644 index 00000000..7a4539df --- /dev/null +++ b/spec/models/mediaflux/http/asset_approve_request_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe Mediaflux::Http::AssetApproveRequest, type: :model, connect_to_mediaflux: true do + let(:approver) { FactoryBot.create :sysadmin } + let(:session_id) { approver.mediaflux_session } + let(:approved_project) do + project = FactoryBot.create :approved_project + mediaflux_id = project.save_in_mediaflux(session_id: ) + meta = ProjectMetadata.new(project: project, current_user: approver) + data_sponsor = User.find_by(uid: project.metadata[:data_sponsor]) + project.provenance_events.create(event_type: ProvenanceEvent::SUBMISSION_EVENT_TYPE, event_person: data_sponsor.uid, event_details: "Requested by #{data_sponsor.display_name_safe}") + meta.approve_project(params: { mediaflux_id:, project_directory_prefix: "tigerns/test", project_directory: "approved_project", + storage_capacity: { size: { requested: 200, approved: 100 }, unit: { requested: "PB", approved: "TB" } }, + storage_performance_expectations: { requested: "Standard", approved: "Fast" } }) + project + end + + let(:approve_request_xml) do + filename = Rails.root.join("spec", "fixtures", "files", "asset_approve_request.xml") + File.new(filename).read + end + + let(:approve_response_xml) do + filename = Rails.root.join("spec", "fixtures", "files", "asset_approve_response.xml") + File.new(filename).read + end + + describe "#resolve" do + it "updates the submission" do + approve_request = described_class.new(session_token: session_id, project: approved_project) + approve_request.resolve + req = Mediaflux::Http::AssetMetadataRequest.new(session_token: session_id, id: approved_project.mediaflux_id) + metadata = req.metadata + expect(req.error?).to be_falsey + approval = approved_project.provenance_events.find_by(event_type: ProvenanceEvent::APPROVAL_EVENT_TYPE) + expect(metadata[:submission][:approved_by]).to eq(approval.event_person) + submission = approved_project.provenance_events.find_by(event_type: ProvenanceEvent::SUBMISSION_EVENT_TYPE) + expect(metadata[:submission][:requested_by]).to eq(submission.event_person) + end + end +end diff --git a/spec/models/project_mediaflux_spec.rb b/spec/models/project_mediaflux_spec.rb index 074580aa..2b9eff9b 100644 --- a/spec/models/project_mediaflux_spec.rb +++ b/spec/models/project_mediaflux_spec.rb @@ -109,7 +109,8 @@ "requested"=>project.metadata[:storage_capacity][:size][:requested]}, "unit"=>{"approved"=>"GB", "requested"=>"GB"}}, event_note: "Other", - event_note_message: "Message filler" + event_note_message: "Message filler", + storage_performance_expectations: { "requested" => "standard" } } project_metadata.approve_project(params:) session_token = current_user.mediaflux_session @@ -134,7 +135,8 @@ "requested"=>project.metadata[:storage_capacity][:size][:requested]}, "unit"=>{"approved"=>"GB", "requested"=>"GB"}}, event_note: "Other", - event_note_message: "Message filler" + event_note_message: "Message filler", + storage_performance_expectations: { "requested" => "standard" } } project_metadata.approve_project(params:) session_token = current_user.mediaflux_session diff --git a/spec/models/project_metadata_spec.rb b/spec/models/project_metadata_spec.rb index a53f2b74..e4c30f66 100644 --- a/spec/models/project_metadata_spec.rb +++ b/spec/models/project_metadata_spec.rb @@ -152,6 +152,7 @@ storage_capacity: {"size"=>{"approved"=>600, "requested"=>project.metadata[:storage_capacity][:size][:requested]}, "unit"=>{"approved"=>"GB", "requested"=>"GB"}}, + storage_performance_expectations: { requested: "Standard", approved: "Fast" }, event_note: "Other", event_note_message: "Message filler" } @@ -169,6 +170,7 @@ storage_capacity: {"size"=>{"approved"=>600, "requested"=>project.metadata[:storage_capacity][:size][:requested]}, "unit"=>{"approved"=>"GB", "requested"=>"GB"}}, + storage_performance_expectations: { requested: "Standard", approved: "Fast" }, event_note: "Other", event_note_message: "Message filler" } @@ -194,6 +196,7 @@ storage_capacity: {"size"=>{"approved"=>600, "requested"=>project.metadata[:storage_capacity][:size][:requested]}, "unit"=>{"approved"=>"GB", "requested"=>"GB"}}, + storage_performance_expectations: { requested: "Standard", approved: "Fast" }, event_note: "Other", event_note_message: "Message filler" } @@ -244,6 +247,7 @@ storage_capacity: {"size"=>{"approved"=>600, "requested"=>project.metadata[:storage_capacity][:size][:requested]}, "unit"=>{"approved"=>"GB", "requested"=>"GB"}}, + storage_performance_expectations: { requested: "Standard", approved: "Fast" }, event_note: "Other", event_note_message: "Message filler" }