diff --git a/app/models/repository_object.rb b/app/models/repository_object.rb index 68bb9d813..869284233 100644 --- a/app/models/repository_object.rb +++ b/app/models/repository_object.rb @@ -90,12 +90,14 @@ def update_opened_version_from(cocina_object:) reload # Syncs up head_version and opened_version end - def open_version!(description:) + # @param [String] description for the version + # @param [RepositoryObjectVersion,nil] from_version existing version to base the new version on. If nil, then uses last_closed_version. + def open_version!(description:, from_version: nil) raise VersionAlreadyOpened, "Cannot open new version because one is already open: #{head_version.version}" if open? RepositoryObject.transaction do - new_version = last_closed_version.dup - new_version.update!(version: new_version.version + 1, version_description: description, closed_at: nil) + new_version = (from_version || last_closed_version).dup + new_version.update!(version: last_closed_version.version + 1, version_description: description, closed_at: nil) update!(opened_version: new_version, head_version: new_version) end end diff --git a/app/services/version_service.rb b/app/services/version_service.rb index c41487932..37b189784 100644 --- a/app/services/version_service.rb +++ b/app/services/version_service.rb @@ -13,12 +13,9 @@ def self.open?(...) new(...).open? end - def self.open(cocina_object:, description:, opening_user_name: nil, assume_accessioned: false) + def self.open(cocina_object:, description:, opening_user_name: nil, assume_accessioned: false, from_version: nil) new(druid: cocina_object.externalIdentifier, version: cocina_object.version) - .open(description:, - opening_user_name:, - assume_accessioned:, - cocina_object:) + .open(description:, opening_user_name:, assume_accessioned:, cocina_object:, from_version:) end def self.can_open?(druid:, version:, assume_accessioned: false) @@ -48,17 +45,20 @@ def initialize(druid:, version:) # @param [String] description set description of version change (required) # @param [String] opening_user_name add opening username to the events datastream (optional) # @param [Boolean] assume_accessioned If true, does not check whether object has been accessioned. + # @param [Integer,nil] from_version existing version to base the new version on, otherwise uses last_closed_version # @return [Cocina::Models::DROWithMetadata, Cocina::Models::AdminPolicyWithMetadata, Cocina::Models::CollectionWithMetadata] updated cocina object # @raise [VersionService::VersioningError] if the object hasn't been accessioned, or if a version is already opened # @raise [Preservation::Client::Error] if bad response from preservation catalog. - def open(cocina_object:, description:, assume_accessioned:, opening_user_name: nil) + def open(cocina_object:, description:, assume_accessioned:, opening_user_name: nil, from_version: nil) raise ArgumentError, 'description is required to open a new version' if description.blank? ensure_openable!(assume_accessioned:) repository_object = RepositoryObject.find_by!(external_identifier: cocina_object.externalIdentifier) - check_version!(current_version: repository_object.head_version.version) + check_version!(current_version: repository_object.head_version.version) unless from_version - repository_object.open_version!(description:) + from_repository_object_version = from_version ? repository_object.versions.find_by!(version: from_version) : nil + + repository_object.open_version!(description:, from_version: from_repository_object_version) # Reloading to get correct lock value. Indexer.reindex_later(cocina_object: repository_object.reload.to_cocina_with_metadata) diff --git a/lib/tasks/dor_services_app.rake b/lib/tasks/dor_services_app.rake index 37c792fc6..522540fcc 100644 --- a/lib/tasks/dor_services_app.rake +++ b/lib/tasks/dor_services_app.rake @@ -5,4 +5,22 @@ namespace :dsa do task embargo_release: :environment do EmbargoReleaseService.release_all end + + desc 'Open a new version from an existing version' + task :open_version, %i[druid description from_version] => :environment do |_task, args| + repository_object = RepositoryObject.find_by!(external_identifier: args[:druid]) + VersionService.open(cocina_object: repository_object.to_cocina, description: args[:description], + assume_accessioned: false, from_version: args[:from_version].to_i) + end + + desc 'Move a user version' + task :move_user_version, %i[druid user_version to_version] => :environment do |_task, args| + UserVersionService.move(druid: args[:druid], version: args[:to_version].to_i, user_version: args[:user_version].to_i) + end + + desc 'Closes a repository object without changing a user version' + task :close_version, %i[druid] => :environment do |_task, args| + repository_object = RepositoryObject.find_by!(external_identifier: args[:druid]) + VersionService.close(druid: args[:druid], version: repository_object.head_version.version, user_version_mode: :none) + end end diff --git a/spec/models/repository_object_spec.rb b/spec/models/repository_object_spec.rb index c890f28df..0e325af92 100644 --- a/spec/models/repository_object_spec.rb +++ b/spec/models/repository_object_spec.rb @@ -143,6 +143,26 @@ expect(newly_created_version.closed_at).to be_nil end end + + context 'when based on an earlier version' do + before do + repository_object.head_version.label = 'Version 1' + repository_object.close_version! + repository_object.open_version!(description: 'Another version') + repository_object.head_version.label = 'Version 2' + repository_object.close_version! + end + + it 'creates a new version and updates the head and opened version pointers' do + expect { repository_object.open_version!(description:, from_version: repository_object.versions.first) }.to change(RepositoryObjectVersion, :count).by(1) + newly_created_version = repository_object.versions.last + expect(newly_created_version.version).to eq(3) + expect(repository_object.head_version).to eq(newly_created_version) + expect(repository_object.opened_version).to eq(newly_created_version) + expect(newly_created_version.label).to eq 'Version 1' + expect(newly_created_version.closed_at).to be_nil + end + end end describe '#close_version!' do diff --git a/spec/services/version_service_spec.rb b/spec/services/version_service_spec.rb index 9ee7e534a..b30890a25 100644 --- a/spec/services/version_service_spec.rb +++ b/spec/services/version_service_spec.rb @@ -17,7 +17,9 @@ end describe '.open' do - subject(:open) { described_class.open(cocina_object:, description: 'same as it ever was', opening_user_name: 'sunetid') } + subject(:open) { described_class.open(cocina_object:, description: 'same as it ever was', opening_user_name: 'sunetid', from_version:) } + + let(:from_version) { nil } let(:workflow_client) do instance_double(Dor::Workflow::Client, create_workflow_by_name: true) @@ -90,6 +92,32 @@ expect { open }.to raise_error(VersionService::VersioningError, errmsg) end end + + context 'when a from version is provided' do + let(:from_version) { 1 } + + before do + repository_object.open_version!(description: 'A new version') + repository_object.head_version.update!(label: 'New version label') + repository_object.close_version! + end + + it 'creates an object version and starts a workflow' do + expect(open).to be_a(Cocina::Models::DROWithMetadata) + expect(open.label).not_to eq 'New version label' + expect(workflow_state_service).to have_received(:accessioned?) + expect(workflow_state_service).to have_received(:accessioning?) + expect(workflow_client).to have_received(:create_workflow_by_name).with(druid, 'versioningWF', version: '3') + + expect(EventFactory).to have_received(:create).with(data: { version: '3', who: 'sunetid' }, + druid:, + event_type: 'version_open') + + expect(Indexer).to have_received(:reindex_later).with(cocina_object: repository_object.reload.to_cocina_with_metadata) + expect(repository_object.opened_version.version).to eq 3 + expect(repository_object.opened_version.version_description).to eq 'same as it ever was' + end + end end describe '.open?' do