Skip to content

Commit

Permalink
Merge pull request #603 from thoth-pub/feature/595_require_publicatio…
Browse files Browse the repository at this point in the history
…n_date_active_work

Add Crossmark metadata to Crossref DOI deposit, reduce list of work_status, require publication date for Active works
  • Loading branch information
brendan-oconnell authored Sep 4, 2024
2 parents 5e73182 + e18eb5e commit 40f8c5e
Show file tree
Hide file tree
Showing 23 changed files with 943 additions and 336 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]
### Added
- [595](https://github.com/thoth-pub/thoth/issues/595) - Remove infrequently used and unused work statuses (unspecified, no longer our product, out of stock indefinitely, out of print, inactive, unknown, remaindered, recalled). Require a publication date for active, withdrawn from sale, and superseded works in Thoth. Add a new `Superseded` work status to replace Out of Print for older editions of Works. Require a withdrawn from sale date for Superseded works.
- [582](https://github.com/thoth-pub/thoth/issues/582) - Add Crossmark metadata in Crossref DOI deposit when a Crossmark policy is present in the publisher record. Add Crossmark update new_edition metadata when a book is replaced by a new edition, and withdrawal metadata when a book is withdrawn from sale.
- [574](https://github.com/thoth-pub/thoth/issues/574) - Add descriptions to all remaining items in schema

### Fixed
Expand Down
49 changes: 49 additions & 0 deletions thoth-api/migrations/v0.12.9/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
ALTER TABLE work
-- Drop constraints originally from v0.12.3,
-- otherwise it won't be able to cast to text
DROP CONSTRAINT IF EXISTS work_inactive_no_withdrawn_date_check,
DROP CONSTRAINT IF EXISTS work_active_withdrawn_date_check,
-- Drop new constraint from v.0.12.9
DROP CONSTRAINT IF EXISTS work_active_publication_date_check;

ALTER TABLE work ALTER COLUMN work_status TYPE text;

-- !!! if this down migration is run, 'out-of-print' should
-- be treated as a placeholder work_status.
-- Works will need to be manually reassigned correct work_status:
-- out-of-print, out-of-stock-indefinitely, or inactive
-- This needs to be run because superseded is a new work_status
-- that is removed in this down migration.
UPDATE work
SET work_status = 'out-of-print'
WHERE work_status = 'superseded';

DROP TYPE work_status;

CREATE TYPE work_status AS ENUM (
'unspecified',
'cancelled',
'forthcoming',
'postponed-indefinitely',
'active',
'no-longer-our-product',
'out-of-stock-indefinitely',
'out-of-print',
'inactive',
'unknown',
'remaindered',
'withdrawn-from-sale',
'recalled'
);

ALTER TABLE work ALTER COLUMN work_status TYPE work_status USING work_status::work_status;

-- add constraints back to work table
ALTER TABLE work
ADD CONSTRAINT work_active_withdrawn_date_check CHECK
((work_status = 'withdrawn-from-sale' OR work_status = 'out-of-print')
OR (work_status NOT IN ('withdrawn-from-sale', 'out-of-print') AND withdrawn_date IS NULL)),

ADD CONSTRAINT work_inactive_no_withdrawn_date_check CHECK
(((work_status = 'withdrawn-from-sale' OR work_status = 'out-of-print') AND withdrawn_date IS NOT NULL)
OR (work_status NOT IN ('withdrawn-from-sale', 'out-of-print')));
102 changes: 102 additions & 0 deletions thoth-api/migrations/v0.12.9/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
-- Assign 1900-01-01 as placeholder publication_date for
-- Active, withdrawn from sale, out of print, out of stock indefinitely works with no publication date
-- Required for work_active_publication_date_check constraint below
-- Affected works in production db with this status, 29-05-2024: 59 works (incl. chapters)
-- Before running migration, make a list of affected works
-- After running migration, publishers should be notified to add correct publication_date
-- !!! This is irreversible
UPDATE work
SET
publication_date = '1900-01-01'
WHERE
work_status IN
('active', 'withdrawn-from-sale', 'out-of-print', 'out-of-stock-indefinitely', 'inactive')
AND publication_date IS NULL;

-- Drop constraints, otherwise it won't be able to cast to text
ALTER TABLE work
DROP CONSTRAINT IF EXISTS work_active_withdrawn_date_check,
DROP CONSTRAINT IF EXISTS work_inactive_no_withdrawn_date_check;

ALTER TABLE work ALTER COLUMN work_status TYPE text;

-- delete unused work_status enum
DROP TYPE work_status;

-- Assign out of print/inactive/out of stock indefinitely works work_status 'superseded'
-- current counts in production db as of 29-05-2024:
-- 145 works (incl. chapters)
-- Before running migration, make a list of affected works
-- After running migration, publishers should be notified to add correct work_status
-- and remove withdrawn_date as necessary. Many OBP "out of print" works are actually first editions
-- for which superseded is the correct new work_status.
-- !!! This is irreversible
UPDATE work
SET
work_status = 'superseded',
-- assign a withdrawn_date, which is required for superseded works
withdrawn_date = CASE
WHEN withdrawn_date IS NOT NULL THEN withdrawn_date
-- + INTERVAL '1 day' is necessary because at least one work has publication_date on
-- the same day as updated_at, but updated_at has a timestamp, so it's
-- greater than. Which then throws an error with the
-- work_withdrawn_date_after_publication_date_check constraint.
WHEN withdrawn_date IS NULL AND publication_date + INTERVAL '1 day' < updated_at THEN updated_at
ELSE CURRENT_DATE
END
WHERE
work_status = 'out-of-print'
OR work_status = 'out-of-stock-indefinitely'
OR work_status = 'inactive';

-- Assign unspecified/unkown works work_status 'forthcoming'
-- current counts in production db as of 29-05-2024:
-- unspecified, 0 works
-- unknown, 0 works
-- !!! This is irreversible
UPDATE work
SET work_status = 'forthcoming'
WHERE work_status = 'unspecified' OR work_status = 'unknown';

-- Assign no longer our product/remaindered/recalled works work_status 'withdrawn-from-sale'
-- current counts in production db as of 29-05-2024:
-- no-longer-our-product, 0 works
-- remaindered, 0 works
-- recalled, 0 works
-- !!! This is irreversible
UPDATE work
SET
work_status = 'withdrawn-from-sale',
withdrawn_date = COALESCE(withdrawn_date, updated_at)
WHERE
work_status = 'no-longer-our-product'
OR work_status = 'remaindered'
OR work_status = 'recalled';

-- create new work_status enum, adds superseded
CREATE TYPE work_status AS ENUM (
'cancelled',
'forthcoming',
'postponed-indefinitely',
'active',
'withdrawn-from-sale',
'superseded'
);
ALTER TABLE work ALTER COLUMN work_status TYPE work_status USING work_status::work_status;

-- add new constraints (with same names as in v0.12.3) to work table
ALTER TABLE work
-- withdrawn and superseded works must have withdrawn_date
-- note that this constraint has the same name as migration from v.0.12.3,
-- but changes previous constraint by adding superseded alongside withdrawn-from-sale
ADD CONSTRAINT work_inactive_no_withdrawn_date_check CHECK
(((work_status = 'withdrawn-from-sale' OR work_status = 'superseded') AND withdrawn_date IS NOT NULL)
OR (work_status NOT IN ('withdrawn-from-sale', 'superseded'))),
-- all other work statuses must not have withdrawn_date; see above, adds superseded
ADD CONSTRAINT work_active_withdrawn_date_check CHECK
((work_status = 'withdrawn-from-sale' OR work_status = 'superseded')
OR (work_status NOT IN ('withdrawn-from-sale', 'superseded') AND withdrawn_date IS NULL)),
-- active, withdrawn-from-sale, and superseded works must have publication_date
ADD CONSTRAINT work_active_publication_date_check CHECK
((work_status IN ('active', 'withdrawn-from-sale', 'superseded') AND publication_date IS NOT NULL)
OR (work_status NOT IN ('active', 'withdrawn-from-sale', 'superseded')));
1 change: 1 addition & 0 deletions thoth-api/src/model/work/crud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ where
Self: WorkProperties,
{
fn validate(&self) -> ThothResult<()> {
self.active_withdrawn_superseded_no_publication_date_error()?;
self.withdrawn_date_error()?;
self.no_withdrawn_date_error()?;
self.withdrawn_date_before_publication_date_error()
Expand Down
101 changes: 36 additions & 65 deletions thoth-api/src/model/work/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,25 +82,15 @@ pub enum WorkType {
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[strum(serialize_all = "title_case")]
pub enum WorkStatus {
Unspecified,
Cancelled,
#[default]
Forthcoming,
#[cfg_attr(feature = "backend", db_rename = "postponed-indefinitely")]
PostponedIndefinitely,
Active,
#[cfg_attr(feature = "backend", db_rename = "no-longer-our-product")]
NoLongerOurProduct,
#[cfg_attr(feature = "backend", db_rename = "out-of-stock-indefinitely")]
OutOfStockIndefinitely,
#[cfg_attr(feature = "backend", db_rename = "out-of-print")]
OutOfPrint,
#[default]
Inactive,
Unknown,
Remaindered,
#[cfg_attr(feature = "backend", db_rename = "withdrawn-from-sale")]
WithdrawnFromSale,
Recalled,
Superseded,
#[cfg_attr(feature = "backend", db_rename = "postponed-indefinitely")]
PostponedIndefinitely,
Cancelled,
}

#[cfg_attr(
Expand Down Expand Up @@ -363,8 +353,14 @@ pub struct WorkOrderBy {
}

impl WorkStatus {
fn is_withdrawn_out_of_print(&self) -> bool {
matches!(self, WorkStatus::OutOfPrint | WorkStatus::WithdrawnFromSale)
fn is_withdrawn_superseded(&self) -> bool {
matches!(self, WorkStatus::WithdrawnFromSale | WorkStatus::Superseded)
}
fn is_active_withdrawn_superseded(&self) -> bool {
matches!(
self,
WorkStatus::Active | WorkStatus::WithdrawnFromSale | WorkStatus::Superseded
)
}
}

Expand All @@ -373,23 +369,38 @@ pub trait WorkProperties {
fn publication_date(&self) -> &Option<NaiveDate>;
fn withdrawn_date(&self) -> &Option<NaiveDate>;

fn is_withdrawn_out_of_print(&self) -> bool {
self.work_status().is_withdrawn_out_of_print()
fn is_withdrawn_superseded(&self) -> bool {
self.work_status().is_withdrawn_superseded()
}

fn is_active_withdrawn_superseded(&self) -> bool {
self.work_status().is_active_withdrawn_superseded()
}

fn has_withdrawn_date(&self) -> bool {
self.withdrawn_date().is_some()
}

fn has_publication_date(&self) -> bool {
self.publication_date().is_some()
}

fn active_withdrawn_superseded_no_publication_date_error(&self) -> ThothResult<()> {
if self.is_active_withdrawn_superseded() && !self.has_publication_date() {
return Err(ThothError::PublicationDateError);
}
Ok(())
}

fn withdrawn_date_error(&self) -> ThothResult<()> {
if !self.is_withdrawn_out_of_print() && self.has_withdrawn_date() {
if !self.is_withdrawn_superseded() && self.has_withdrawn_date() {
return Err(ThothError::WithdrawnDateError);
}
Ok(())
}

fn no_withdrawn_date_error(&self) -> ThothResult<()> {
if self.is_withdrawn_out_of_print() && !self.has_withdrawn_date() {
if self.is_withdrawn_superseded() && !self.has_withdrawn_date() {
return Err(ThothError::NoWithdrawnDateError);
}
Ok(())
Expand Down Expand Up @@ -545,7 +556,7 @@ fn test_worktype_default() {
#[test]
fn test_workstatus_default() {
let workstatus: WorkStatus = Default::default();
assert_eq!(workstatus, WorkStatus::Inactive);
assert_eq!(workstatus, WorkStatus::Forthcoming);
}

#[test]
Expand Down Expand Up @@ -573,23 +584,11 @@ fn test_workstatus_display() {
"Postponed Indefinitely"
);
assert_eq!(format!("{}", WorkStatus::Active), "Active");
assert_eq!(
format!("{}", WorkStatus::NoLongerOurProduct),
"No Longer Our Product"
);
assert_eq!(
format!("{}", WorkStatus::OutOfStockIndefinitely),
"Out Of Stock Indefinitely"
);
assert_eq!(format!("{}", WorkStatus::OutOfPrint), "Out Of Print");
assert_eq!(format!("{}", WorkStatus::Inactive), "Inactive");
assert_eq!(format!("{}", WorkStatus::Unknown), "Unknown");
assert_eq!(format!("{}", WorkStatus::Remaindered), "Remaindered");
assert_eq!(
format!("{}", WorkStatus::WithdrawnFromSale),
"Withdrawn From Sale"
);
assert_eq!(format!("{}", WorkStatus::Recalled), "Recalled");
assert_eq!(format!("{}", WorkStatus::Superseded), "Superseded");
}

#[test]
Expand Down Expand Up @@ -667,10 +666,6 @@ fn test_worktype_fromstr() {
#[test]
fn test_workstatus_fromstr() {
use std::str::FromStr;
assert_eq!(
WorkStatus::from_str("Unspecified").unwrap(),
WorkStatus::Unspecified
);
assert_eq!(
WorkStatus::from_str("Cancelled").unwrap(),
WorkStatus::Cancelled
Expand All @@ -684,37 +679,13 @@ fn test_workstatus_fromstr() {
WorkStatus::PostponedIndefinitely
);
assert_eq!(WorkStatus::from_str("Active").unwrap(), WorkStatus::Active);
assert_eq!(
WorkStatus::from_str("No Longer Our Product").unwrap(),
WorkStatus::NoLongerOurProduct
);
assert_eq!(
WorkStatus::from_str("Out Of Stock Indefinitely").unwrap(),
WorkStatus::OutOfStockIndefinitely
);
assert_eq!(
WorkStatus::from_str("Out Of Print").unwrap(),
WorkStatus::OutOfPrint
);
assert_eq!(
WorkStatus::from_str("Inactive").unwrap(),
WorkStatus::Inactive
);
assert_eq!(
WorkStatus::from_str("Unknown").unwrap(),
WorkStatus::Unknown
);
assert_eq!(
WorkStatus::from_str("Remaindered").unwrap(),
WorkStatus::Remaindered
);
assert_eq!(
WorkStatus::from_str("Withdrawn From Sale").unwrap(),
WorkStatus::WithdrawnFromSale
);
assert_eq!(
WorkStatus::from_str("Recalled").unwrap(),
WorkStatus::Recalled
WorkStatus::from_str("Superseded").unwrap(),
WorkStatus::Superseded
);

assert!(WorkStatus::from_str("Published").is_err());
Expand Down
15 changes: 9 additions & 6 deletions thoth-app/src/component/new_work.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ impl Component for NewWorkComponent {
self.work.page_interval = None;
}
if self.work.work_status != WorkStatus::WithdrawnFromSale
&& self.work.work_status != WorkStatus::OutOfPrint
&& self.work.work_status != WorkStatus::Superseded
{
self.work.withdrawn_date = None;
}
Expand Down Expand Up @@ -459,9 +459,11 @@ impl Component for NewWorkComponent {
// Grey out chapter-specific or "book"-specific fields
// based on currently selected work type.
let is_chapter = self.work.work_type == WorkType::BookChapter;
let is_not_withdrawn_or_out_of_print = self.work.work_status
!= WorkStatus::WithdrawnFromSale
&& self.work.work_status != WorkStatus::OutOfPrint;
let is_not_withdrawn_or_superseded = self.work.work_status != WorkStatus::WithdrawnFromSale
&& self.work.work_status != WorkStatus::Superseded;
let is_active_withdrawn_or_superseded = self.work.work_status == WorkStatus::Active
|| self.work.work_status == WorkStatus::WithdrawnFromSale
|| self.work.work_status == WorkStatus::Superseded;
html! {
<>
<nav class="level">
Expand Down Expand Up @@ -528,13 +530,14 @@ impl Component for NewWorkComponent {
label = "Publication Date"
value={ self.work.publication_date.clone() }
oninput={ ctx.link().callback(|e: InputEvent| Msg::ChangeDate(e.to_value())) }
required={ is_active_withdrawn_or_superseded }
/>
<FormDateInput
label = "Withdrawn Date"
value={ self.work.withdrawn_date.clone() }
oninput={ ctx.link().callback(|e: InputEvent| Msg::ChangeWithdrawnDate(e.to_value())) }
required = true
deactivated={ is_not_withdrawn_or_out_of_print }
required = { !is_not_withdrawn_or_superseded }
deactivated={ is_not_withdrawn_or_superseded }
/>
<FormTextInput
label = "Place of Publication"
Expand Down
Loading

0 comments on commit 40f8c5e

Please sign in to comment.