From de94f4d11ba320f8a6be0e544e5c7b979a51ed19 Mon Sep 17 00:00:00 2001 From: Brendan O'Connell Date: Mon, 8 Apr 2024 13:12:09 +0200 Subject: [PATCH 01/12] Added change to changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a87f499..cde1a646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + - [583](https://github.com/thoth-pub/thoth/issues/583) - Add new field, Permanently Withdrawn Date, to Work for Out-of-print or Withdrawn from Sale Works. ## [[0.12.0]](https://github.com/thoth-pub/thoth/releases/tag/v0.12.0) - 2024-03-14 ### Removed From 0d57c3b52e66d900a6df7c2dfa39c5cb96eaac89 Mon Sep 17 00:00:00 2001 From: Brendan O'Connell Date: Thu, 11 Apr 2024 11:14:50 +0200 Subject: [PATCH 02/12] Added up and down migrations, added validation error logic, added error messages to database_errors.rs and lib.rs, updated schema, updated model --- thoth-api/migrations/v0.11.20/down.sql | 4 ++ thoth-api/migrations/v0.11.20/up.sql | 13 +++++ thoth-api/src/graphql/model.rs | 7 +++ thoth-api/src/model/work/crud.rs | 20 +++++++- thoth-api/src/model/work/mod.rs | 70 ++++++++++++++++++++++++++ thoth-api/src/schema.rs | 1 + thoth-errors/src/database_errors.rs | 1 + thoth-errors/src/lib.rs | 4 ++ 8 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 thoth-api/migrations/v0.11.20/down.sql create mode 100644 thoth-api/migrations/v0.11.20/up.sql diff --git a/thoth-api/migrations/v0.11.20/down.sql b/thoth-api/migrations/v0.11.20/down.sql new file mode 100644 index 00000000..e9b5c49a --- /dev/null +++ b/thoth-api/migrations/v0.11.20/down.sql @@ -0,0 +1,4 @@ +ALTER TABLE work + DROP CONSTRAINT work_withdrawn_date_check, + DROP COLUMN withdrawn_date; + \ No newline at end of file diff --git a/thoth-api/migrations/v0.11.20/up.sql b/thoth-api/migrations/v0.11.20/up.sql new file mode 100644 index 00000000..4257e211 --- /dev/null +++ b/thoth-api/migrations/v0.11.20/up.sql @@ -0,0 +1,13 @@ +ALTER TABLE work + ADD COLUMN withdrawn_date DATE; + +UPDATE work + SET withdrawn_date = updated_at + WHERE (work_status = 'withdrawn-from-sale' + OR work_status = 'out-of-print'); + +ALTER TABLE work + ADD CONSTRAINT work_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') AND withdrawn_date IS NULL)); \ No newline at end of file diff --git a/thoth-api/src/graphql/model.rs b/thoth-api/src/graphql/model.rs index 6f608b9c..0ca0defc 100644 --- a/thoth-api/src/graphql/model.rs +++ b/thoth-api/src/graphql/model.rs @@ -2279,6 +2279,13 @@ impl Work { self.publication_date } + #[graphql( + description = "Date a work was withdrawn from publication. Only applies to out of print and withdrawn from sale works." + )] + pub fn withdrawn_date(&self) -> Option { + self.withdrawn_date + } + pub fn place(&self) -> Option<&String> { self.place.as_ref() } diff --git a/thoth-api/src/model/work/crud.rs b/thoth-api/src/model/work/crud.rs index 160272ab..6d587577 100644 --- a/thoth-api/src/model/work/crud.rs +++ b/thoth-api/src/model/work/crud.rs @@ -1,6 +1,5 @@ use super::{ - NewWork, NewWorkHistory, PatchWork, Work, WorkField, WorkHistory, WorkOrderBy, WorkStatus, - WorkType, + NewWork, NewWorkHistory, PatchWork, WorkProperties, Work, WorkField, WorkHistory, WorkOrderBy, WorkStatus, WorkType }; use crate::graphql::model::TimeExpression; use crate::graphql::utils::{Direction, Expression}; @@ -176,6 +175,10 @@ impl Crud for Work { Direction::Asc => query.order(dsl::publication_date.asc()), Direction::Desc => query.order(dsl::publication_date.desc()), }, + WorkField::WithdrawnDate => match order.direction { + Direction::Asc => query.order(dsl::withdrawn_date.asc()), + Direction::Desc => query.order(dsl::withdrawn_date.desc()), + }, WorkField::Place => match order.direction { Direction::Asc => query.order(dsl::place.asc()), Direction::Desc => query.order(dsl::place.desc()), @@ -399,6 +402,19 @@ impl DbInsert for NewWorkHistory { db_insert!(work_history::table); } +pub trait WorkValidation +where + Self: WorkProperties, +{ + fn validate(&self) -> ThothResult<()> { + self.withdrawn_date_error() + } +} + +impl WorkValidation for NewWork {} + +impl WorkValidation for PatchWork {} + #[cfg(test)] mod tests { use super::*; diff --git a/thoth-api/src/model/work/mod.rs b/thoth-api/src/model/work/mod.rs index 2fc57d54..51ea4ec3 100644 --- a/thoth-api/src/model/work/mod.rs +++ b/thoth-api/src/model/work/mod.rs @@ -1,5 +1,6 @@ use chrono::naive::NaiveDate; use serde::{Deserialize, Serialize}; +use thoth_errors::{ThothError, ThothResult}; use std::fmt; use strum::Display; use strum::EnumString; @@ -98,6 +99,7 @@ pub enum WorkField { #[strum(serialize = "DOI")] Doi, PublicationDate, + WithdrawnDate, Place, PageCount, PageBreakdown, @@ -144,6 +146,7 @@ pub struct Work { pub imprint_id: Uuid, pub doi: Option, pub publication_date: Option, + pub withdrawn_date: Option, pub place: Option, pub page_count: Option, pub page_breakdown: Option, @@ -184,6 +187,7 @@ pub struct WorkWithRelations { pub edition: Option, pub doi: Option, pub publication_date: Option, + pub withdrawn_date: Option, pub place: Option, pub page_count: Option, pub page_breakdown: Option, @@ -234,6 +238,7 @@ pub struct NewWork { pub imprint_id: Uuid, pub doi: Option, pub publication_date: Option, + pub withdrawn_date: Option, pub place: Option, pub page_count: Option, pub page_breakdown: Option, @@ -275,6 +280,7 @@ pub struct PatchWork { pub imprint_id: Uuid, pub doi: Option, pub publication_date: Option, + pub withdrawn_date: Option, pub place: Option, pub page_count: Option, pub page_breakdown: Option, @@ -326,6 +332,67 @@ pub struct WorkOrderBy { pub direction: Direction, } +impl WorkStatus { + fn is_withdrawn_out_of_print(&self) -> bool { + matches!(self, WorkStatus::OutOfPrint | WorkStatus::WithdrawnFromSale) + } + + fn is_not_withdrawn_out_of_print(&self) -> bool { + !self.is_withdrawn_out_of_print() + } +} + +pub trait WorkProperties { + fn work_status(&self) -> &WorkStatus; + fn withdrawn_date(&self) -> &Option; + + fn is_not_withdrawn_out_of_print(&self) -> bool { + self.work_status().is_not_withdrawn_out_of_print() + } + + fn has_withdrawn_date(&self) -> bool { + self.withdrawn_date().is_some() + } + + fn withdrawn_date_error(&self) -> ThothResult<()> { + if self.is_not_withdrawn_out_of_print() { + if self.has_withdrawn_date() { + return Err(ThothError::WithdrawnDateError) + } + } + Ok(()) + } +} + +impl WorkProperties for Work { + fn work_status(&self) -> &WorkStatus { + &self.work_status + } + + fn withdrawn_date(&self) -> &Option { + &self.withdrawn_date + } +} +impl WorkProperties for NewWork { + fn work_status(&self) -> &WorkStatus { + &self.work_status + } + + fn withdrawn_date(&self) -> &Option { + &self.withdrawn_date + } +} + +impl WorkProperties for PatchWork { + fn work_status(&self) -> &WorkStatus { + &self.work_status + } + + fn withdrawn_date(&self) -> &Option { + &self.withdrawn_date + } +} + impl Work { pub fn compile_fulltitle(&self) -> String { if let Some(subtitle) = &self.subtitle.clone() { @@ -376,6 +443,7 @@ impl From for PatchWork { imprint_id: w.imprint_id, doi: w.doi, publication_date: w.publication_date, + withdrawn_date: w.withdrawn_date, place: w.place, page_count: w.page_count, page_breakdown: w.page_breakdown, @@ -727,6 +795,7 @@ fn test_work_into_patchwork() { imprint_id: Uuid::parse_str("00000000-0000-0000-BBBB-000000000002").unwrap(), doi: Some(Doi::from_str("https://doi.org/10.00001/BOOK.0001").unwrap()), publication_date: chrono::NaiveDate::from_ymd_opt(1999, 12, 31), + withdrawn_date: chrono::NaiveDate::from_ymd_opt(2000, 12, 31), place: Some("León, Spain".to_string()), page_count: Some(123), page_breakdown: None, @@ -766,6 +835,7 @@ fn test_work_into_patchwork() { assert_eq!(work.imprint_id, patch_work.imprint_id); assert_eq!(work.doi, patch_work.doi); assert_eq!(work.publication_date, patch_work.publication_date); + assert_eq!(work.withdrawn_date, patch_work.withdrawn_date); assert_eq!(work.place, patch_work.place); assert_eq!(work.page_count, patch_work.page_count); assert_eq!(work.page_breakdown, patch_work.page_breakdown); diff --git a/thoth-api/src/schema.rs b/thoth-api/src/schema.rs index cada1078..8e393a8b 100644 --- a/thoth-api/src/schema.rs +++ b/thoth-api/src/schema.rs @@ -530,6 +530,7 @@ table! { imprint_id -> Uuid, doi -> Nullable, publication_date -> Nullable, + withdrawn_date -> Nullable, place -> Nullable, page_count -> Nullable, page_breakdown -> Nullable, diff --git a/thoth-errors/src/database_errors.rs b/thoth-errors/src/database_errors.rs index c220c096..73837988 100644 --- a/thoth-errors/src/database_errors.rs +++ b/thoth-errors/src/database_errors.rs @@ -160,6 +160,7 @@ static DATABASE_CONSTRAINT_ERRORS: Map<&'static str, &'static str> = phf_map! { "work_title_check" => "Title must not be an empty string.", "work_toc_check" => "Table of content must not be an empty string.", "work_video_count_check" => "A video count must be greater than 0.", + "work_withdrawn_date_check" => "Withdrawn Date can only be added to out of print or withdrawn from sale Works.", }; impl From for ThothError { diff --git a/thoth-errors/src/lib.rs b/thoth-errors/src/lib.rs index cdf5970c..8175cacc 100644 --- a/thoth-errors/src/lib.rs +++ b/thoth-errors/src/lib.rs @@ -84,6 +84,10 @@ pub enum ThothError { #[error( "Price values must be greater than zero. To indicate an unpriced Publication, omit all Prices." )] + WithdrawnDateError, + #[error( + "Withdrawn Date can only be added to out of print or withdrawn from sale Works." + )] PriceZeroError, #[error("{0}")] RequestError(String), From 6b16b62782de5b4a8d72e347a22093879cc10471 Mon Sep 17 00:00:00 2001 From: Brendan O'Connell Date: Fri, 12 Apr 2024 10:53:32 +0200 Subject: [PATCH 03/12] Added withdrawn_date to mutations and queries, added to component --- thoth-api/src/model/work/mod.rs | 8 +++++++- thoth-app/src/component/new_work.rs | 18 ++++++++++++++++++ thoth-app/src/component/work.rs | 19 +++++++++++++++++++ .../src/models/work/create_work_mutation.rs | 3 +++ .../src/models/work/update_work_mutation.rs | 3 +++ thoth-app/src/models/work/work_query.rs | 1 + thoth-app/src/models/work/works_query.rs | 1 + thoth-client/assets/queries.graphql | 2 ++ 8 files changed, 54 insertions(+), 1 deletion(-) diff --git a/thoth-api/src/model/work/mod.rs b/thoth-api/src/model/work/mod.rs index 51ea4ec3..05eb6081 100644 --- a/thoth-api/src/model/work/mod.rs +++ b/thoth-api/src/model/work/mod.rs @@ -373,6 +373,7 @@ impl WorkProperties for Work { &self.withdrawn_date } } + impl WorkProperties for NewWork { fn work_status(&self) -> &WorkStatus { &self.work_status @@ -548,6 +549,7 @@ fn test_workfield_display() { assert_eq!(format!("{}", WorkField::Edition), "Edition"); assert_eq!(format!("{}", WorkField::Doi), "DOI"); assert_eq!(format!("{}", WorkField::PublicationDate), "PublicationDate"); + assert_eq!(format!("{}", WorkField::WithdrawnDate), "WithdrawnDate"); assert_eq!(format!("{}", WorkField::Place), "Place"); assert_eq!(format!("{}", WorkField::PageCount), "PageCount"); assert_eq!(format!("{}", WorkField::PageBreakdown), "PageBreakdown"); @@ -689,6 +691,10 @@ fn test_workfield_fromstr() { WorkField::from_str("PublicationDate").unwrap(), WorkField::PublicationDate ); + assert_eq!( + WorkField::from_str("WithdrawnDate").unwrap(), + WorkField::WithdrawnDate + ); assert_eq!(WorkField::from_str("Place").unwrap(), WorkField::Place); assert_eq!( WorkField::from_str("PageCount").unwrap(), @@ -795,7 +801,7 @@ fn test_work_into_patchwork() { imprint_id: Uuid::parse_str("00000000-0000-0000-BBBB-000000000002").unwrap(), doi: Some(Doi::from_str("https://doi.org/10.00001/BOOK.0001").unwrap()), publication_date: chrono::NaiveDate::from_ymd_opt(1999, 12, 31), - withdrawn_date: chrono::NaiveDate::from_ymd_opt(2000, 12, 31), + withdrawn_date: None, place: Some("León, Spain".to_string()), page_count: Some(123), page_breakdown: None, diff --git a/thoth-app/src/component/new_work.rs b/thoth-app/src/component/new_work.rs index 8978f82b..7b4d1625 100644 --- a/thoth-app/src/component/new_work.rs +++ b/thoth-app/src/component/new_work.rs @@ -97,6 +97,7 @@ pub enum Msg { ChangeEdition(String), ChangeDoi(String), ChangeDate(String), + ChangeWithdrawnDate(String), ChangePlace(String), ChangePageCount(String), ChangePageBreakdown(String), @@ -299,6 +300,7 @@ impl Component for NewWorkComponent { edition: self.work.edition, doi: self.work.doi.clone(), publication_date: self.work.publication_date.clone(), + withdrawn_date: self.work.withdrawn_date.clone(), place: self.work.place.clone(), page_count: self.work.page_count, page_breakdown: self.work.page_breakdown.clone(), @@ -376,6 +378,7 @@ impl Component for NewWorkComponent { } } Msg::ChangeDate(value) => self.work.publication_date.neq_assign(value.to_opt_string()), + Msg::ChangeWithdrawnDate(value) => self.work.withdrawn_date.neq_assign(value.to_opt_string()), Msg::ChangePlace(value) => self.work.place.neq_assign(value.to_opt_string()), Msg::ChangePageCount(value) => self.work.page_count.neq_assign(value.to_opt_int()), Msg::ChangePageBreakdown(value) => { @@ -515,6 +518,21 @@ impl Component for NewWorkComponent { value={ self.work.publication_date.clone() } oninput={ ctx.link().callback(|e: InputEvent| Msg::ChangeDate(e.to_value())) } /> + { + if self.work.work_status == WorkStatus::WithdrawnFromSale || self.work.work_status == WorkStatus::OutOfPrint { + html! { + <> + + + } + } else { + html!{} + } + } self.work.publication_date.neq_assign(value.to_opt_string()), + Msg::ChangeWithdrawnDate(value) => self.work.withdrawn_date.neq_assign(value.to_opt_string()), Msg::ChangePlace(value) => self.work.place.neq_assign(value.to_opt_string()), Msg::ChangePageCount(value) => self.work.page_count.neq_assign(value.to_opt_int()), Msg::ChangePageBreakdown(value) => { @@ -637,6 +640,22 @@ impl Component for WorkComponent { value={ self.work.publication_date.clone() } oninput={ ctx.link().callback(|e: InputEvent| Msg::ChangeDate(e.to_value())) } /> + { + if self.work.work_status == WorkStatus::WithdrawnFromSale || self.work.work_status == WorkStatus::OutOfPrint { + html! { + <> + + + } + } else { + html!{} + } + } + , pub doi: Option, pub publication_date: Option, + pub withdrawn_date: Option, pub place: Option, pub page_count: Option, pub page_breakdown: Option, diff --git a/thoth-app/src/models/work/update_work_mutation.rs b/thoth-app/src/models/work/update_work_mutation.rs index 46acabe0..a11f48de 100644 --- a/thoth-app/src/models/work/update_work_mutation.rs +++ b/thoth-app/src/models/work/update_work_mutation.rs @@ -19,6 +19,7 @@ const UPDATE_WORK_MUTATION: &str = " $imprintId: Uuid!, $doi: Doi, $publicationDate: NaiveDate, + $withdrawnDate: NaiveDate, $place: String, $pageCount: Int, $pageBreakdown: String, @@ -55,6 +56,7 @@ const UPDATE_WORK_MUTATION: &str = " imprintId: $imprintId doi: $doi publicationDate: $publicationDate + withdrawnDate: $withdrawnDate place: $place pageCount: $pageCount pageBreakdown: $pageBreakdown @@ -115,6 +117,7 @@ pub struct Variables { pub edition: Option, pub doi: Option, pub publication_date: Option, + pub withdrawn_date: Option, pub place: Option, pub page_count: Option, pub page_breakdown: Option, diff --git a/thoth-app/src/models/work/work_query.rs b/thoth-app/src/models/work/work_query.rs index b31f4319..a32894fc 100644 --- a/thoth-app/src/models/work/work_query.rs +++ b/thoth-app/src/models/work/work_query.rs @@ -20,6 +20,7 @@ pub const WORK_QUERY: &str = " edition doi publicationDate + withdrawnDate place pageCount pageBreakdown diff --git a/thoth-app/src/models/work/works_query.rs b/thoth-app/src/models/work/works_query.rs index 4b8680b5..68ca4a53 100644 --- a/thoth-app/src/models/work/works_query.rs +++ b/thoth-app/src/models/work/works_query.rs @@ -21,6 +21,7 @@ pub const WORKS_QUERY_BODY: &str = " license place publicationDate + withdrawnDate updatedAt contributions { contributionId diff --git a/thoth-client/assets/queries.graphql b/thoth-client/assets/queries.graphql index b25301c8..2f65dc35 100644 --- a/thoth-client/assets/queries.graphql +++ b/thoth-client/assets/queries.graphql @@ -46,6 +46,7 @@ fragment Work on Work { edition doi publicationDate + withdrawnDate license copyrightHolder shortAbstract @@ -161,6 +162,7 @@ fragment Work on Work { edition doi publicationDate + withdrawnDate license shortAbstract longAbstract From eb6bcaa981b3d190c2f5afe8e9a7af41a16e6840 Mon Sep 17 00:00:00 2001 From: Brendan O'Connell Date: Mon, 15 Apr 2024 11:24:37 +0200 Subject: [PATCH 04/12] Added hook to set withdrawn_date for chapter to same as parent Work --- thoth-api/src/graphql/model.rs | 2 ++ thoth-app/src/component/new_work.rs | 25 ++++++++++--------------- thoth-app/src/component/utils.rs | 3 +++ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/thoth-api/src/graphql/model.rs b/thoth-api/src/graphql/model.rs index 0ca0defc..e8d15aa9 100644 --- a/thoth-api/src/graphql/model.rs +++ b/thoth-api/src/graphql/model.rs @@ -1713,9 +1713,11 @@ impl MutationRoot { for child in work.children(&context.db)? { if child.publication_date != w.publication_date || child.work_status != w.work_status + || child.withdrawn_date != w.withdrawn_date { let mut data: PatchWork = child.clone().into(); data.publication_date = w.publication_date; + data.withdrawn_date = w.withdrawn_date; data.work_status = w.work_status.clone(); child.update(&context.db, &data, &account_id)?; } diff --git a/thoth-app/src/component/new_work.rs b/thoth-app/src/component/new_work.rs index 7b4d1625..dc29ebaf 100644 --- a/thoth-app/src/component/new_work.rs +++ b/thoth-app/src/component/new_work.rs @@ -289,6 +289,9 @@ impl Component for NewWorkComponent { self.work.last_page = None; self.work.page_interval = None; } + if self.work.work_status == WorkStatus::OutOfPrint { + self.work.withdrawn_date = None; + } let body = CreateWorkRequestBody { variables: Variables { work_type: self.work.work_type.clone(), @@ -451,6 +454,7 @@ 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; html! { <>