Skip to content

Commit

Permalink
Merge pull request #5160 from sul-dlss/t5158-permanent_withdraw
Browse files Browse the repository at this point in the history
Support permanently withdrawing user versions.
  • Loading branch information
jcoyne authored Aug 8, 2024
2 parents 0e9b16d + 5436267 commit 66e235f
Show file tree
Hide file tree
Showing 15 changed files with 213 additions and 26 deletions.
2 changes: 1 addition & 1 deletion app/jobs/withdraw_restore_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class WithdrawRestoreJob < ApplicationJob
def perform(user_version:)
druid = user_version.repository_object_version.repository_object.external_identifier
version = user_version.version
if user_version.withdrawn
if user_version.withdrawn?
PurlFetcher::Client::Withdraw.withdraw(druid:, version:)
else
PurlFetcher::Client::Withdraw.restore(druid:, version:)
Expand Down
16 changes: 12 additions & 4 deletions app/models/user_version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ class UserVersion < ApplicationRecord
belongs_to :repository_object_version
before_save :set_version

enum :state, withdrawn: 'withdrawn', available: 'available', permanently_withdrawn: 'permanently_withdrawn'

validate :repository_object_version_is_closed
validate :repository_object_version_has_cocina
validate :can_withdraw
validate :when_permanently_withdrawn

def repository_object_version_is_closed
# Validate that the repository object version is closed
Expand All @@ -21,26 +24,31 @@ def repository_object_version_has_cocina

def can_withdraw
# Validate that the user version can be withdrawn or restored
errors.add(:repository_object_version, 'head version cannot be withdrawn') if withdrawn && (head? || version.nil?)
errors.add(:repository_object_version, 'head version cannot be withdrawn') if withdrawn? && (head? || version.nil?)
end

def when_permanently_withdrawn
# Validate that the user version state cannot be changed from permanently withdrawn
errors.add(:repository_object_version, 'cannot set user version state when permanently withdrawn') if changed_attributes['state'] == 'permanently_withdrawn'
end

def as_json
{
userVersion: version,
version: repository_object_version.version,
withdrawn:,
withdrawn: withdrawn?,
withdrawable: withdrawable?,
restorable: restorable?,
head: head?
}
end

def withdrawable?
!withdrawn && !head?
available? && !head?
end

def restorable?
withdrawn
withdrawn?
end

def head?
Expand Down
20 changes: 19 additions & 1 deletion app/services/user_version_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def self.create(druid:, version:)
# @raise [UserVersionService::UserVersioningError] RepositoryObjectVersion not found for the version
def self.withdraw(druid:, user_version:, withdraw: true)
user_version = user_version_for(druid:, user_version:)
user_version.update!(withdrawn: withdraw)
user_version.update!(state: withdraw ? 'withdrawn' : 'available')
WithdrawRestoreJob.perform_later(user_version:)
EventFactory.create(druid:, event_type: 'user_version_withdrawn', data: { version: user_version.to_s, withdrawn: withdraw })
user_version
Expand Down Expand Up @@ -76,6 +76,24 @@ def self.object_version_for(druid:, user_version:)
user_version_for(druid:, user_version:).repository_object_version.version
end

# Mark all UserVersions other than the latest as permanently withdrawn
# @param [String] druid of the item
# @raise [UserVersionService::UserVersioningError] RepositoryObject not found for the druid
def self.permanently_withdraw_previous_user_versions(druid:)
# No need to notify Purl-Fetcher; it will delete the user versions because the object is being made dark.
repository_object = repository_object(druid:)
latest_user_version = latest_user_version(druid:)
RepositoryObject.transaction do
repository_object.user_versions.each do |user_version|
next if user_version.version == latest_user_version
next if user_version.permanently_withdrawn?

user_version.permanently_withdrawn!
EventFactory.create(druid:, event_type: 'user_version_permanently_withdrawn', data: { version: user_version.to_s })
end
end
end

# private below

# @return [RepositoryObject] The repository object for the druid
Expand Down
10 changes: 10 additions & 0 deletions app/services/version_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def close(description:, user_name:, start_accession: true, user_version_mode: DE
EventFactory.create(druid:, event_type: 'version_close', data: { who: user_name, version: version.to_s })

update_user_version(user_version_mode:, repository_object:)
update_previous_user_versions(repository_object:)
end

# Determines whether a version can be closed for an object.
Expand Down Expand Up @@ -212,5 +213,14 @@ def check_version!(current_version:)

raise VersionService::VersioningError, "Version from Preservation is out of sync. Preservation expects #{preservation_version} but current version is #{current_version}"
end

def update_previous_user_versions(repository_object:)
return unless repository_object.user_versions.length > 1

cocina_object = repository_object.to_cocina
return unless cocina_object.access.view == 'dark'

UserVersionService.permanently_withdraw_previous_user_versions(druid:)
end
end
# rubocop:enable Metrics/ClassLength
11 changes: 11 additions & 0 deletions db/migrate/20240807210223_change_user_version_state.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class ChangeUserVersionState < ActiveRecord::Migration[7.1]
def change
add_column :user_versions, :state, :string, default: 'available', null: false

execute <<~SQL
UPDATE user_versions SET state = 'withdrawn' WHERE withdrawn = true;
SQL

remove_column :user_versions, :withdrawn
end
end
12 changes: 10 additions & 2 deletions db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;

--
-- Name: public; Type: SCHEMA; Schema: -; Owner: -
--

-- *not* creating schema, since initdb creates it


--
-- Name: background_job_result_status; Type: TYPE; Schema: public; Owner: -
--
Expand Down Expand Up @@ -340,10 +347,10 @@ ALTER SEQUENCE public.tag_labels_id_seq OWNED BY public.tag_labels.id;
CREATE TABLE public.user_versions (
id bigint NOT NULL,
version integer NOT NULL,
withdrawn boolean DEFAULT false NOT NULL,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL,
repository_object_version_id bigint NOT NULL
repository_object_version_id bigint NOT NULL,
state character varying DEFAULT 'available'::character varying NOT NULL
);


Expand Down Expand Up @@ -719,6 +726,7 @@ ALTER TABLE ONLY public.repository_objects
SET search_path TO "$user", public;

INSERT INTO "schema_migrations" (version) VALUES
('20240807210223'),
('20240531122304'),
('20240522142556'),
('20240430144139'),
Expand Down
2 changes: 1 addition & 1 deletion spec/factories/repository_object_versions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
is_member_of { [] }
source_id { "sul:#{SecureRandom.uuid}" }
end
version { 1 }
sequence(:version)
version_description { 'Best version ever' }
lock { 2 }
cocina_version { Cocina::Models::VERSION }
Expand Down
3 changes: 2 additions & 1 deletion spec/factories/user_versions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
FactoryBot.define do
factory :user_version do
sequence(:version)
withdrawn { false }
state { 'available' }
repository_object_version
end
end
6 changes: 3 additions & 3 deletions spec/jobs/withdraw_restore_job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
end

let(:druid) { 'druid:mk420bs7601' }
let(:user_version) { create(:user_version, withdrawn:, repository_object_version:) }
let(:user_version) { create(:user_version, state:, repository_object_version:) }
let(:repository_object_version) { create(:repository_object_version, :with_repository_object, external_identifier: druid, closed_at: Time.current) }

context 'when the version is withdrawn' do
let(:withdrawn) { true }
let(:state) { 'withdrawn' }

before do
allow(PurlFetcher::Client::Withdraw).to receive(:withdraw)
Expand All @@ -25,7 +25,7 @@
end

context 'when the version is not withdrawn' do
let(:withdrawn) { false }
let(:state) { 'available' }

before do
allow(PurlFetcher::Client::Withdraw).to receive(:restore)
Expand Down
63 changes: 60 additions & 3 deletions spec/models/user_version_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

let(:attrs) do
{
version: 1,
version_description: 'My new version',
closed_at: Time.current
}
Expand All @@ -32,7 +31,6 @@
context 'when the repository object version is open' do
let(:attrs) do
{
version: 1,
version_description: 'My new version'
}
end
Expand All @@ -47,9 +45,68 @@
end

context 'when the user version cannot be withdrawn' do
let(:user_version) { build(:user_version, repository_object_version:, version: nil, withdrawn: true) }
let(:user_version) { build(:user_version, repository_object_version:, version: nil, state: 'withdrawn') }

it { is_expected.to include 'Repository object version head version cannot be withdrawn' }
end

context 'when the user version is permanently withdrawn' do
let(:user_version) do
user_version = create(:user_version, repository_object_version:, state: 'permanently_withdrawn')
user_version.state = 'available'
user_version
end

it { is_expected.to include 'Repository object version cannot set user version state when permanently withdrawn' }
end
end

describe '#as_json' do
let(:user_version) { build(:user_version, repository_object_version:, state:) }

context 'when the user version is withdrawn' do
let(:state) { 'withdrawn' }

it 'returns the user version as JSON' do
expect(user_version.as_json).to eq(
userVersion: user_version.version,
version: user_version.repository_object_version.version,
withdrawn: true,
withdrawable: false,
restorable: true,
head: false
)
end
end

context 'when the user version is permanently withdrawn' do
let(:state) { 'permanently_withdrawn' }

it 'returns the user version as JSON' do
expect(user_version.as_json).to eq(
userVersion: user_version.version,
version: user_version.repository_object_version.version,
withdrawn: false,
withdrawable: false,
restorable: false,
head: false
)
end
end

context 'when the user version is available' do
let(:state) { 'available' }

it 'returns the user version as JSON' do
expect(user_version.as_json).to eq(
userVersion: user_version.version,
version: user_version.repository_object_version.version,
withdrawn: false,
withdrawable: true,
restorable: false,
head: false
)
end
end
end
end
4 changes: 2 additions & 2 deletions spec/requests/create_user_version_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
params: { version: repository_object_version.version }.to_json

expect(response).to have_http_status(:created)
expect(response.parsed_body).to eq({ 'userVersion' => 1, 'version' => 1, 'withdrawn' => false, 'withdrawable' => false, 'restorable' => false, 'head' => true })

expect(repository_object_version.reload.user_versions.count).to eq(1)
user_version = repository_object_version.user_versions.first
expect(response.parsed_body).to eq({ 'userVersion' => user_version.version, 'version' => repository_object_version.version, 'withdrawn' => false, 'withdrawable' => false, 'restorable' => false, 'head' => true })
end
end

Expand Down
4 changes: 2 additions & 2 deletions spec/requests/update_user_version_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

RSpec.describe 'Update user version' do
let(:repository_object) { repository_object_version1.repository_object }
let(:repository_object_version1) { create(:repository_object_version, :with_repository_object, closed_at: Time.zone.now) }
let(:repository_object_version1) { create(:repository_object_version, :with_repository_object, closed_at: Time.zone.now, version: 1) }
let!(:repository_object_version2) { create(:repository_object_version, version: 2, repository_object:, closed_at: Time.zone.now) }
let(:user_version) { create(:user_version, repository_object_version: repository_object_version1, version: 1) }

Expand Down Expand Up @@ -34,7 +34,7 @@
expect(response).to have_http_status(:ok)
expect(response.parsed_body).to eq({ 'userVersion' => user_version.version, 'version' => 1, 'withdrawn' => true, 'withdrawable' => false, 'restorable' => true, 'head' => false })

expect(user_version.reload.withdrawn).to be(true)
expect(user_version.reload.withdrawn?).to be(true)
end
end

Expand Down
4 changes: 2 additions & 2 deletions spec/services/migrators/remove_release_tags_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
RSpec.describe Migrators::RemoveReleaseTags do
subject(:migrator) { described_class.new(repository_object) }

let(:repository_object) { create(:repository_object, :with_repository_object_version, repository_object_version:) }
let(:repository_object_version) { build(:repository_object_version, administrative:) }
let(:repository_object) { repository_object_version.repository_object }
let(:repository_object_version) { build(:repository_object_version, :with_repository_object, administrative:) }
let(:administrative) { { hasAdminPolicy: 'druid:hy787xj5878', releaseTags: [] } }

describe '#migrate?' do
Expand Down
29 changes: 25 additions & 4 deletions spec/services/user_version_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
RSpec.describe UserVersionService do
let(:druid) { repository_object.external_identifier }
let(:repository_object) { repository_object_version1.repository_object }
let(:repository_object_version1) { create(:repository_object_version, :with_repository_object, closed_at: Time.zone.now) }
let(:repository_object_version1) { create(:repository_object_version, :with_repository_object, closed_at: Time.zone.now, version: 1) }
let!(:repository_object_version2) { create(:repository_object_version, version: 2, repository_object:, closed_at: Time.zone.now) }
let(:object_type) { 'dro' }

before do
allow(EventFactory).to receive(:create)
end

describe '.create' do
subject(:user_version_service_create) { described_class.create(druid:, version: 1) }

Expand Down Expand Up @@ -55,16 +59,16 @@
end

it 'withdraws the user version' do
expect(user_version.withdrawn).to be false
expect(user_version.withdrawn?).to be false
user_version_service_withdraw
expect(user_version.reload.withdrawn).to be true
expect(user_version.reload.withdrawn?).to be true
expect(WithdrawRestoreJob).to have_received(:perform_later).with(user_version:)
end
end

context 'when the user version cannot be withdrawn' do
it 'raises' do
expect(user_version.withdrawn).to be false
expect(user_version.withdrawn?).to be false
expect { user_version_service_withdraw }.to raise_error UserVersionService::UserVersioningError, 'Validation failed: Repository object version head version cannot be withdrawn'
expect(WithdrawRestoreJob).not_to have_received(:perform_later)
end
Expand Down Expand Up @@ -97,4 +101,21 @@
expect(user_version_service_exist?).to be false
end
end

describe '.permanently_withdraw_previous_user_versions' do
subject(:user_version_service_permanently_withdraw) { described_class.permanently_withdraw_previous_user_versions(druid:) }

let!(:user_version1) { UserVersion.create!(version: 1, repository_object_version: repository_object_version1, state: 'permanently_withdrawn') }
let!(:user_version2) { UserVersion.create!(version: 2, repository_object_version: repository_object_version1) }
let!(:user_version3) { UserVersion.create!(version: 3, repository_object_version: repository_object_version1) }

it 'permanently withdraws the previous user versions' do
user_version_service_permanently_withdraw
expect(user_version1.reload.permanently_withdrawn?).to be true
expect(user_version2.reload.permanently_withdrawn?).to be true
expect(user_version3.reload.available?).to be true

expect(EventFactory).to have_received(:create).once
end
end
end
Loading

0 comments on commit 66e235f

Please sign in to comment.