diff --git a/CHANGELOG.md b/CHANGELOG.md index 718fe66fd..a1dc8ef86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,23 @@ All notable changes to thoth will be documented in this file. 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). +## [[0.4.6]](https://github.com/thoth-pub/thoth/releases/tag/v0.4.6) - 2021-09-02 +### Added + - [#88](https://github.com/thoth-pub/thoth/issues/88) - Implement KBART specification + - [#266](https://github.com/thoth-pub/thoth/issues/266) - Delete confirmation to publications + +### Changed + - [#272](https://github.com/thoth-pub/thoth/issues/272) - Use more fields in `contributors` filtering + +### Fixed + - [#271](https://github.com/thoth-pub/thoth/issues/271) - Make filter parameter optional in `subjectCount` + + ## [[0.4.5]](https://github.com/thoth-pub/thoth/releases/tag/v0.4.5) - 2021-08-12 ### Added - [#259](https://github.com/thoth-pub/thoth/issues/259) - Units selection dropdown to Work and NewWork pages, which updates the Width/Height display on change - [#259](https://github.com/thoth-pub/thoth/issues/259) - Local storage key to retain user's choice of units across all Work/NewWork pages - - [#259](https://github.com/thoth-pub/thoth/issues/259) - Backend function to convert to/from database units (mm): uses 1inch = 25.4mm as conversion factor, rounds mm values to nearest mm, rounds cm values to 1 decimal place, rounds inch values to 2 decimal places and then to nearest sixteenth of an inch + - [#259](https://github.com/thoth-pub/thoth/issues/259) - Backend function to convert to/from database units (mm): uses 1inch = 25.4mm as conversion factor, rounds mm values to nearest mm, rounds cm values to 1 decimal place, rounds inch values to 2 decimal places - [#259](https://github.com/thoth-pub/thoth/issues/259) - Constraints on Width/Height fields depending on unit selection: user may only enter whole numbers when in mm, numbers with up to 1 decimal place when in cm, numbers with up to 2 decimal places when in inches ### Changed diff --git a/Cargo.lock b/Cargo.lock index b4436396b..56ca9929e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3936,7 +3936,7 @@ dependencies = [ [[package]] name = "thoth" -version = "0.4.5" +version = "0.4.6" dependencies = [ "cargo-husky", "clap", @@ -3951,7 +3951,7 @@ dependencies = [ [[package]] name = "thoth-api" -version = "0.4.5" +version = "0.4.6" dependencies = [ "actix-web", "argon2rs", @@ -3980,7 +3980,7 @@ dependencies = [ [[package]] name = "thoth-api-server" -version = "0.4.5" +version = "0.4.6" dependencies = [ "actix-cors", "actix-identity", @@ -3995,7 +3995,7 @@ dependencies = [ [[package]] name = "thoth-app" -version = "0.4.5" +version = "0.4.6" dependencies = [ "anyhow", "chrono", @@ -4018,7 +4018,7 @@ dependencies = [ [[package]] name = "thoth-app-server" -version = "0.4.5" +version = "0.4.6" dependencies = [ "actix-cors", "actix-web", @@ -4027,7 +4027,7 @@ dependencies = [ [[package]] name = "thoth-client" -version = "0.4.5" +version = "0.4.6" dependencies = [ "chrono", "graphql_client", @@ -4041,7 +4041,7 @@ dependencies = [ [[package]] name = "thoth-errors" -version = "0.4.5" +version = "0.4.6" dependencies = [ "actix-web", "csv", @@ -4056,7 +4056,7 @@ dependencies = [ [[package]] name = "thoth-export-server" -version = "0.4.5" +version = "0.4.6" dependencies = [ "actix-cors", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index db39823a1..230da6ad8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth" -version = "0.4.5" +version = "0.4.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" @@ -16,11 +16,11 @@ maintenance = { status = "actively-developed" } members = ["thoth-api", "thoth-api-server", "thoth-app", "thoth-app-server", "thoth-client", "thoth-errors", "thoth-export-server"] [dependencies] -thoth-api = { version = "0.4.5", path = "thoth-api", features = ["backend"] } -thoth-api-server = { version = "0.4.5", path = "thoth-api-server" } -thoth-app-server = { version = "0.4.5", path = "thoth-app-server" } -thoth-errors = { version = "0.4.5", path = "thoth-errors" } -thoth-export-server = { version = "0.4.5", path = "thoth-export-server" } +thoth-api = { version = "0.4.6", path = "thoth-api", features = ["backend"] } +thoth-api-server = { version = "0.4.6", path = "thoth-api-server" } +thoth-app-server = { version = "0.4.6", path = "thoth-app-server" } +thoth-errors = { version = "0.4.6", path = "thoth-errors" } +thoth-export-server = { version = "0.4.6", path = "thoth-export-server" } clap = "2.33.3" dialoguer = "0.7.1" dotenv = "0.9.0" diff --git a/thoth-api-server/Cargo.toml b/thoth-api-server/Cargo.toml index 7dea4a3b0..ef597e7c9 100644 --- a/thoth-api-server/Cargo.toml +++ b/thoth-api-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-api-server" -version = "0.4.5" +version = "0.4.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" @@ -9,8 +9,8 @@ repository = "https://github.com/thoth-pub/thoth" readme = "README.md" [dependencies] -thoth-api = { version = "0.4.5", path = "../thoth-api", features = ["backend"] } -thoth-errors = { version = "0.4.5", path = "../thoth-errors" } +thoth-api = { version = "0.4.6", path = "../thoth-api", features = ["backend"] } +thoth-errors = { version = "0.4.6", path = "../thoth-errors" } actix-web = "3.3.2" actix-cors = "0.5.4" actix-identity = "0.3.1" diff --git a/thoth-api/Cargo.toml b/thoth-api/Cargo.toml index 238114ade..028172f3b 100644 --- a/thoth-api/Cargo.toml +++ b/thoth-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-api" -version = "0.4.5" +version = "0.4.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" @@ -16,7 +16,7 @@ maintenance = { status = "actively-developed" } backend = ["diesel", "diesel-derive-enum", "diesel_migrations", "futures", "actix-web"] [dependencies] -thoth-errors = { version = "0.4.5", path = "../thoth-errors" } +thoth-errors = { version = "0.4.6", path = "../thoth-errors" } actix-web = { version = "3.3.2", optional = true } argon2rs = "0.2.5" isbn2 = "0.4.0" diff --git a/thoth-api/src/contributor/crud.rs b/thoth-api/src/contributor/crud.rs index 35444747e..3737b6e6f 100644 --- a/thoth-api/src/contributor/crud.rs +++ b/thoth-api/src/contributor/crud.rs @@ -77,6 +77,7 @@ impl Crud for Contributor { query = query.filter( full_name .ilike(format!("%{}%", filter)) + .or(last_name.ilike(format!("%{}%", filter))) .or(orcid.ilike(format!("%{}%", filter))), ); } @@ -104,6 +105,7 @@ impl Crud for Contributor { query = query.filter( full_name .ilike(format!("%{}%", filter)) + .or(last_name.ilike(format!("%{}%", filter))) .or(orcid.ilike(format!("%{}%", filter))), ); } diff --git a/thoth-api/src/graphql/model.rs b/thoth-api/src/graphql/model.rs index c3a9436d2..a6e3e6d65 100644 --- a/thoth-api/src/graphql/model.rs +++ b/thoth-api/src/graphql/model.rs @@ -415,7 +415,7 @@ impl QueryRoot { offset(default = 0, description = "The number of items to skip"), filter( default = "".to_string(), - description = "A query string to search. This argument is a test, do not rely on it. At present it simply searches for case insensitive literals on full_name and orcid" + description = "A query string to search. This argument is a test, do not rely on it. At present it simply searches for case insensitive literals on full_name, last_name and orcid" ), order( default = ContributorOrderBy::default(), @@ -455,7 +455,7 @@ impl QueryRoot { arguments( filter( default = "".to_string(), - description = "A query string to search. This argument is a test, do not rely on it. At present it simply searches for case insensitive literals on full_name and orcid", + description = "A query string to search. This argument is a test, do not rely on it. At present it simply searches for case insensitive literals on full_name, last_name and orcid", ), ) )] @@ -815,7 +815,16 @@ impl QueryRoot { Subject::from_id(&context.db, &subject_id).map_err(|e| e.into()) } - #[graphql(description = "Get the total number of subjects associated to works")] + #[graphql( + description = "Get the total number of subjects associated to works", + arguments( + filter( + default = "".to_string(), + description = "A query string to search. This argument is a test, do not rely on it. At present it simply searches for case insensitive literals on subject_code", + ), + subject_type(description = "A specific type to filter by"), + ) + )] fn subject_count( context: &Context, filter: String, diff --git a/thoth-app-server/Cargo.toml b/thoth-app-server/Cargo.toml index c10c63bac..70a641d11 100644 --- a/thoth-app-server/Cargo.toml +++ b/thoth-app-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-app-server" -version = "0.4.5" +version = "0.4.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" diff --git a/thoth-app/Cargo.toml b/thoth-app/Cargo.toml index 30038a703..c5d63e9a8 100644 --- a/thoth-app/Cargo.toml +++ b/thoth-app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-app" -version = "0.4.5" +version = "0.4.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" @@ -33,5 +33,5 @@ serde = { version = "1.0.115", features = ["derive"] } serde_json = "1.0" url = "2.1.1" uuid = { version = "0.7", features = ["serde", "v4"] } -thoth-api = { version = "0.4.5", path = "../thoth-api" } -thoth-errors = { version = "0.4.5", path = "../thoth-errors" } +thoth-api = { version = "0.4.6", path = "../thoth-api" } +thoth-errors = { version = "0.4.6", path = "../thoth-errors" } diff --git a/thoth-app/manifest.json b/thoth-app/manifest.json index e5029b3a5..21caea6b0 100644 --- a/thoth-app/manifest.json +++ b/thoth-app/manifest.json @@ -9,7 +9,7 @@ "start_url": "/?homescreen=1", "background_color": "#ffffff", "theme_color": "#ffdd57", - "version": "0.4.5", + "version": "0.4.6", "icons": [ { "src": "\/android-icon-36x36.png", diff --git a/thoth-app/src/component/publication.rs b/thoth-app/src/component/publication.rs index 7c5649c49..12af937e1 100644 --- a/thoth-app/src/component/publication.rs +++ b/thoth-app/src/component/publication.rs @@ -18,6 +18,7 @@ use crate::agent::notification_bus::NotificationBus; use crate::agent::notification_bus::NotificationDispatcher; use crate::agent::notification_bus::NotificationStatus; use crate::agent::notification_bus::Request; +use crate::component::delete_dialogue::ConfirmDeleteComponent; use crate::component::prices_form::PricesFormComponent; use crate::component::utils::Loader; use crate::models::publication::delete_publication_mutation::DeletePublicationRequest; @@ -32,7 +33,6 @@ use crate::models::publication::publication_query::PublicationRequestBody; use crate::models::publication::publication_query::Variables; use crate::route::AdminRoute; use crate::route::AppRoute; -use crate::string::DELETE_BUTTON; pub struct PublicationComponent { publication: PublicationWithRelations, @@ -219,9 +219,15 @@ impl Component for PublicationComponent {

- +

diff --git a/thoth-app/src/models/work/mod.rs b/thoth-app/src/models/work/mod.rs index efc9f420f..6fd7e7e0c 100644 --- a/thoth-app/src/models/work/mod.rs +++ b/thoth-app/src/models/work/mod.rs @@ -119,6 +119,7 @@ pub trait DisplayWork { fn onix_projectmuse_endpoint(&self) -> String; fn onix_oapen_endpoint(&self) -> String; fn csv_endpoint(&self) -> String; + fn kbart_endpoint(&self) -> String; fn cover_alt_text(&self) -> String; fn license_icons(&self) -> Html; fn status_tag(&self) -> Html; @@ -147,6 +148,13 @@ impl DisplayWork for WorkWithRelations { ) } + fn kbart_endpoint(&self) -> String { + format!( + "{}/specifications/kbart::oclc/work/{}", + THOTH_EXPORT_API, &self.work_id + ) + } + fn cover_alt_text(&self) -> String { format!("{} - Cover Image", &self.title) } @@ -340,7 +348,7 @@ impl DisplayWork for WorkWithRelations { href={self.onix_projectmuse_endpoint()} class="dropdown-item" > - {"ONIX (Project Muse)"} + {"ONIX (Project MUSE)"} {"CSV"} + + {"KBART"} + diff --git a/thoth-client/Cargo.toml b/thoth-client/Cargo.toml index 430e6df57..2e9d38198 100644 --- a/thoth-client/Cargo.toml +++ b/thoth-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-client" -version = "0.4.5" +version = "0.4.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" @@ -9,8 +9,8 @@ repository = "https://github.com/thoth-pub/thoth" readme = "README.md" [dependencies] -thoth-api = {version = "0.4.5", path = "../thoth-api" } -thoth-errors = {version = "0.4.5", path = "../thoth-errors" } +thoth-api = {version = "0.4.6", path = "../thoth-api" } +thoth-errors = {version = "0.4.6", path = "../thoth-errors" } graphql_client = "0.9.0" chrono = { version = "0.4", features = ["serde"] } reqwest = { version = "0.10", features = ["json"] } diff --git a/thoth-errors/Cargo.toml b/thoth-errors/Cargo.toml index 60eeab314..180c30c0f 100644 --- a/thoth-errors/Cargo.toml +++ b/thoth-errors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-errors" -version = "0.4.5" +version = "0.4.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" diff --git a/thoth-export-server/Cargo.toml b/thoth-export-server/Cargo.toml index f9655b1ce..71f9ed8d2 100644 --- a/thoth-export-server/Cargo.toml +++ b/thoth-export-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-export-server" -version = "0.4.5" +version = "0.4.6" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" @@ -9,9 +9,9 @@ repository = "https://github.com/thoth-pub/thoth" readme = "README.md" [dependencies] -thoth-api = { version = "0.4.5", path = "../thoth-api" } -thoth-errors = { version = "0.4.5", path = "../thoth-errors" } -thoth-client = { version = "0.4.5", path = "../thoth-client" } +thoth-api = { version = "0.4.6", path = "../thoth-api" } +thoth-errors = { version = "0.4.6", path = "../thoth-errors" } +thoth-client = { version = "0.4.6", path = "../thoth-client" } actix-web = "3.3.2" actix-cors = "0.5.4" chrono = { version = "0.4", features = ["serde"] } diff --git a/thoth-export-server/src/csv/csv_thoth.rs b/thoth-export-server/src/csv/csv_thoth.rs index 29a5ff49e..3c00213f2 100644 --- a/thoth-export-server/src/csv/csv_thoth.rs +++ b/thoth-export-server/src/csv/csv_thoth.rs @@ -304,6 +304,8 @@ impl CsvCell for WorkFundings { #[cfg(test)] mod tests { use super::*; + use crate::record::DELIMITER_COMMA; + use csv::QuoteStyle; use lazy_static::lazy_static; use std::str::FromStr; use thoth_client::{ @@ -495,7 +497,7 @@ mod tests { #[test] fn test_csv_thoth() { - let to_test = CsvThoth.generate(&[TEST_WORK.clone()]); + let to_test = CsvThoth.generate(&[TEST_WORK.clone()], QuoteStyle::Always, DELIMITER_COMMA); assert_eq!(to_test, Ok(TEST_RESULT.to_string())) } diff --git a/thoth-export-server/src/csv/kbart_oclc.rs b/thoth-export-server/src/csv/kbart_oclc.rs new file mode 100644 index 000000000..396a6a020 --- /dev/null +++ b/thoth-export-server/src/csv/kbart_oclc.rs @@ -0,0 +1,326 @@ +use csv::Writer; +use serde::Serialize; +use std::convert::TryFrom; +use std::io::Write; +use thoth_client::{ContributionType, PublicationType, Work, WorkType}; +use thoth_errors::{ThothError, ThothResult}; + +use super::{CsvRow, CsvSpecification}; + +pub(crate) struct KbartOclc; + +#[derive(Debug, Serialize)] +struct KbartOclcRow { + publication_title: String, + print_identifier: Option, + online_identifier: Option, + date_first_issue_online: Option, + num_first_vol_online: Option, + num_first_issue_online: Option, + date_last_issue_online: Option, + num_last_vol_online: Option, + num_last_issue_online: Option, + title_url: String, + first_author: Option, + title_id: Option, + embargo_info: Option, + coverage_depth: String, + notes: Option, + publisher_name: Option, + publication_type: String, + date_monograph_published_print: Option, + date_monograph_published_online: i64, + monograph_volume: Option, + monograph_edition: Option, + first_editor: Option, + parent_publication_title_id: Option, + preceding_publication_title_id: Option, + access_type: String, +} + +impl CsvSpecification for KbartOclc { + fn handle_event(w: &mut Writer, works: &[Work]) -> ThothResult<()> { + match works.len() { + 0 => Err(ThothError::IncompleteMetadataRecord( + "onix_3.0::project_muse".to_string(), + "Not enough data".to_string(), + )), + 1 => CsvRow::::csv_row(works.first().unwrap(), w), + _ => { + for work in works.iter() { + CsvRow::::csv_row(work, w).ok(); + } + Ok(()) + } + } + } +} + +impl CsvRow for Work { + fn csv_row(&self, w: &mut Writer) -> ThothResult<()> { + w.serialize(KbartOclcRow::try_from(self.clone())?) + .map_err(|e| e.into()) + } +} + +impl TryFrom for KbartOclcRow { + type Error = ThothError; + + fn try_from(work: Work) -> ThothResult { + // title_url is mandatory in KBART but optional in Thoth + if work.landing_page.is_none() { + Err(ThothError::IncompleteMetadataRecord( + "kbart::oclc".to_string(), + "Missing Landing Page".to_string(), + )) + // Don't output works with no publication date (mandatory in KBART) + } else if work.publication_date.is_none() { + Err(ThothError::IncompleteMetadataRecord( + "kbart::oclc".to_string(), + "Missing Publication Date".to_string(), + )) + } else { + let mut print_identifier = None; + let mut online_identifier = None; + let mut print_edition_exists = false; + for publication in work.publications { + if publication.publication_type == PublicationType::PDF + && publication.isbn.is_some() + { + online_identifier = publication.isbn.clone(); + } + if publication.publication_type == PublicationType::PAPERBACK { + print_edition_exists = true; + if publication.isbn.is_some() { + print_identifier = publication.isbn.clone(); + } + } + if publication.publication_type == PublicationType::HARDBACK { + print_edition_exists = true; + } + } + let mut first_author = None; + let mut first_editor = None; + let mut contributions = work.contributions; + // The first author/editor will usually be the contributor with contribution_ordinal 1, + // but this is not guaranteed, so we select the highest-ranked contributor of the + // appropriate contribution type who is listed as a "main" contributor. + contributions.sort_by(|a, b| a.contribution_ordinal.cmp(&b.contribution_ordinal)); + for contribution in contributions { + if contribution.main_contribution { + if work.work_type == WorkType::EDITED_BOOK { + if contribution.contribution_type == ContributionType::EDITOR { + first_editor = Some(contribution.last_name); + break; + } + } else if contribution.contribution_type == ContributionType::AUTHOR { + first_author = Some(contribution.last_name); + break; + } + } + } + let date_monograph_published_online = work + .publication_date + .map(|date| chrono::Datelike::year(&date).into()) + .unwrap(); + let date_monograph_published_print = match print_edition_exists { + true => Some(date_monograph_published_online), + false => None, + }; + Ok(KbartOclcRow { + publication_title: match work.subtitle { + Some(subtitle) => format!("{}: {}", work.title, subtitle), + None => work.full_title, + }, + print_identifier, + online_identifier, + date_first_issue_online: None, + num_first_vol_online: None, + num_first_issue_online: None, + date_last_issue_online: None, + num_last_vol_online: None, + num_last_issue_online: None, + title_url: work.landing_page.unwrap(), + first_author, + title_id: work.doi, + embargo_info: None, + coverage_depth: "fulltext".to_string(), + notes: None, + publisher_name: Some(work.imprint.publisher.publisher_name), + publication_type: match work.work_type { + WorkType::BOOK_SET => "Serial".to_string(), + _ => "Monograph".to_string(), + }, + date_monograph_published_print, + date_monograph_published_online, + // Note that it is possible for a work to belong to more than one series. + // Only one series can be listed in KBART, so we select the first one found (if any). + monograph_volume: work.issues.first().map(|i| i.issue_ordinal), + monograph_edition: Some(work.edition), + first_editor, + // This should match the series' `title_id` if also provided in the KBART. + parent_publication_title_id: work + .issues + .first() + .map(|i| i.series.issn_digital.to_string()), + preceding_publication_title_id: None, + access_type: "F".to_string(), + }) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::record::DELIMITER_TAB; + use csv::QuoteStyle; + use lazy_static::lazy_static; + use std::str::FromStr; + use thoth_client::{ + ContributionType, PublicationType, WorkContributions, WorkContributionsContributor, + WorkImprint, WorkImprintPublisher, WorkIssues, WorkIssuesSeries, WorkPublications, + WorkStatus, WorkType, + }; + use uuid::Uuid; + + lazy_static! { + static ref TEST_WORK: Work = Work { + work_id: Uuid::from_str("00000000-0000-0000-AAAA-000000000001").unwrap(), + work_status: WorkStatus::ACTIVE, + full_title: "Book Title: Book Subtitle".to_string(), + title: "Book Title".to_string(), + subtitle: Some("Separate Subtitle".to_string()), + work_type: WorkType::MONOGRAPH, + edition: 1, + doi: Some("https://doi.org/10.00001/BOOK.0001".to_string()), + publication_date: Some(chrono::NaiveDate::from_ymd(1999, 12, 31)), + license: Some("http://creativecommons.org/licenses/by/4.0/".to_string()), + copyright_holder: "Author 1; Author 2".to_string(), + short_abstract: Some("Lorem ipsum dolor sit amet".to_string()), + long_abstract: Some( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit".to_string() + ), + general_note: None, + place: Some("León, Spain".to_string()), + width: Some(156.0), + height: Some(234.0), + page_count: Some(334), + page_breakdown: Some("x+334".to_string()), + image_count: Some(15), + table_count: None, + audio_count: None, + video_count: None, + landing_page: Some("https://www.book.com".to_string()), + toc: None, + lccn: None, + oclc: None, + cover_url: Some("https://www.book.com/cover".to_string()), + cover_caption: None, + imprint: WorkImprint { + imprint_name: "OA Editions Imprint".to_string(), + publisher: WorkImprintPublisher { + publisher_name: "OA Editions".to_string(), + }, + }, + issues: vec![ + WorkIssues { + issue_ordinal: 20, + series: WorkIssuesSeries { + series_type: thoth_client::SeriesType::BOOK_SERIES, + series_name: "Name of series".to_string(), + issn_print: "1234-5678".to_string(), + issn_digital: "8765-4321".to_string(), + series_url: None, + }, + }, + WorkIssues { + issue_ordinal: 50, + series: WorkIssuesSeries { + series_type: thoth_client::SeriesType::BOOK_SERIES, + series_name: "Name of second series".to_string(), + issn_print: "1111-2222".to_string(), + issn_digital: "3333-4444".to_string(), + series_url: None, + }, + } + ], + contributions: vec![ + WorkContributions { + contribution_type: ContributionType::AUTHOR, + first_name: Some("Author".to_string()), + last_name: "First".to_string(), + full_name: "Author First".to_string(), + main_contribution: true, + biography: None, + institution: None, + contribution_ordinal: 1, + contributor: WorkContributionsContributor { + orcid: Some("https://orcid.org/0000-0000-0000-0001".to_string()), + }, + }, + WorkContributions { + contribution_type: ContributionType::AUTHOR, + first_name: Some("Author".to_string()), + last_name: "Second".to_string(), + full_name: "Author Second".to_string(), + main_contribution: true, + biography: None, + institution: None, + contribution_ordinal: 2, + contributor: WorkContributionsContributor { orcid: None }, + }, + ], + languages: vec![], + publications: vec![ + WorkPublications { + publication_id: Uuid::from_str("00000000-0000-0000-BBBB-000000000002").unwrap(), + publication_type: PublicationType::PAPERBACK, + publication_url: Some("https://www.book.com/paperback".to_string()), + isbn: Some("978-1-00000-000-0".to_string()), + prices: vec![], + }, + WorkPublications { + publication_id: Uuid::from_str("00000000-0000-0000-CCCC-000000000003").unwrap(), + publication_type: PublicationType::HARDBACK, + publication_url: Some("https://www.book.com/hardback".to_string()), + isbn: Some("978-1-00000-000-1".to_string()), + prices: vec![], + }, + WorkPublications { + publication_id: Uuid::from_str("00000000-0000-0000-DDDD-000000000004").unwrap(), + publication_type: PublicationType::PDF, + publication_url: Some("https://www.book.com/pdf".to_string()), + isbn: Some("978-1-00000-000-2".to_string()), + prices: vec![], + }, + WorkPublications { + publication_id: Uuid::from_str("00000000-0000-0000-EEEE-000000000005").unwrap(), + publication_type: PublicationType::HTML, + publication_url: Some("https://www.book.com/html".to_string()), + isbn: None, + prices: vec![], + }, + WorkPublications { + publication_id: Uuid::from_str("00000000-0000-0000-FFFF-000000000006").unwrap(), + publication_type: PublicationType::XML, + publication_url: Some("https://www.book.com/xml".to_string()), + isbn: Some("978-1-00000-000-3".to_string()), + prices: vec![], + }, + ], + subjects: vec![], + fundings: vec![], + }; + } + + const TEST_RESULT: &str = "publication_title\tprint_identifier\tonline_identifier\tdate_first_issue_online\tnum_first_vol_online\tnum_first_issue_online\tdate_last_issue_online\tnum_last_vol_online\tnum_last_issue_online\ttitle_url\tfirst_author\ttitle_id\tembargo_info\tcoverage_depth\tnotes\tpublisher_name\tpublication_type\tdate_monograph_published_print\tdate_monograph_published_online\tmonograph_volume\tmonograph_edition\tfirst_editor\tparent_publication_title_id\tpreceding_publication_title_id\taccess_type\nBook Title: Separate Subtitle\t978-1-00000-000-0\t978-1-00000-000-2\t\t\t\t\t\t\thttps://www.book.com\tFirst\thttps://doi.org/10.00001/BOOK.0001\t\tfulltext\t\tOA Editions\tMonograph\t1999\t1999\t20\t1\t\t8765-4321\t\tF\n"; + + #[test] + fn test_kbart_oclc() { + let to_test = + KbartOclc.generate(&[TEST_WORK.clone()], QuoteStyle::Necessary, DELIMITER_TAB); + + assert_eq!(to_test, Ok(TEST_RESULT.to_string())) + } +} diff --git a/thoth-export-server/src/csv/mod.rs b/thoth-export-server/src/csv/mod.rs index c6115c2a1..7b7b5cadd 100644 --- a/thoth-export-server/src/csv/mod.rs +++ b/thoth-export-server/src/csv/mod.rs @@ -4,11 +4,15 @@ use thoth_client::Work; use thoth_errors::{ThothError, ThothResult}; pub(crate) trait CsvSpecification { - const QUOTE_STYLE: QuoteStyle = QuoteStyle::Always; - - fn generate(&self, works: &[Work]) -> ThothResult { + fn generate( + &self, + works: &[Work], + quote_style: QuoteStyle, + delimiter: u8, + ) -> ThothResult { let mut writer = WriterBuilder::new() - .quote_style(Self::QUOTE_STYLE) + .quote_style(quote_style) + .delimiter(delimiter) .from_writer(Vec::new()); Self::handle_event(&mut writer, works) .map(|_| writer.into_inner().map_err(|e| e.error().into())) @@ -32,3 +36,5 @@ pub(crate) trait CsvCell { mod csv_thoth; pub(crate) use csv_thoth::CsvThoth; +mod kbart_oclc; +pub(crate) use kbart_oclc::KbartOclc; diff --git a/thoth-export-server/src/data.rs b/thoth-export-server/src/data.rs index e8e591d7a..2d544d7d5 100644 --- a/thoth-export-server/src/data.rs +++ b/thoth-export-server/src/data.rs @@ -25,6 +25,18 @@ lazy_static! { format: concat!(env!("THOTH_EXPORT_API"), "/formats/csv"), accepted_by: vec![concat!(env!("THOTH_EXPORT_API"), "/platforms/thoth"),], }, + Specification { + id: "kbart::oclc", + name: "OCLC KBART", + format: concat!(env!("THOTH_EXPORT_API"), "/formats/kbart"), + accepted_by: vec![ + concat!(env!("THOTH_EXPORT_API"), "/platforms/oclc_kb"), + concat!(env!("THOTH_EXPORT_API"), "/platforms/proquest_kb"), + concat!(env!("THOTH_EXPORT_API"), "/platforms/proquest_exlibris"), + concat!(env!("THOTH_EXPORT_API"), "/platforms/ebsco_kb"), + concat!(env!("THOTH_EXPORT_API"), "/platforms/jisc_kb"), + ], + }, ]; pub(crate) static ref ALL_PLATFORMS: Vec> = vec![ Platform { @@ -51,6 +63,46 @@ lazy_static! { "/specifications/onix_3.0::oapen" ),], }, + Platform { + id: "oclc_kb", + name: "OCLC KB", + accepts: vec![concat!( + env!("THOTH_EXPORT_API"), + "/specifications/kbart::oclc" + ),], + }, + Platform { + id: "proquest_kb", + name: "ProQuest KB", + accepts: vec![concat!( + env!("THOTH_EXPORT_API"), + "/specifications/kbart::oclc" + ),], + }, + Platform { + id: "proquest_exlibris", + name: "ProQuest ExLibris", + accepts: vec![concat!( + env!("THOTH_EXPORT_API"), + "/specifications/kbart::oclc" + ),], + }, + Platform { + id: "ebsco_kb", + name: "EBSCO KB", + accepts: vec![concat!( + env!("THOTH_EXPORT_API"), + "/specifications/kbart::oclc" + ),], + }, + Platform { + id: "jisc_kb", + name: "JISC KB", + accepts: vec![concat!( + env!("THOTH_EXPORT_API"), + "/specifications/kbart::oclc" + ),], + }, ]; pub(crate) static ref ALL_FORMATS: Vec> = vec![ Format { @@ -74,6 +126,15 @@ lazy_static! { "/specifications/csv::thoth" ),], }, + Format { + id: "kbart", + name: "KBART", + version: None, + specifications: vec![concat!( + env!("THOTH_EXPORT_API"), + "/specifications/kbart::oclc" + ),], + }, ]; } diff --git a/thoth-export-server/src/record.rs b/thoth-export-server/src/record.rs index 917a80a8a..95f3ea91a 100644 --- a/thoth-export-server/src/record.rs +++ b/thoth-export-server/src/record.rs @@ -1,4 +1,5 @@ use actix_web::{http::StatusCode, HttpRequest, Responder}; +use csv::QuoteStyle; use paperclip::actix::web::HttpResponse; use paperclip::actix::OperationModifier; use paperclip::util::{ready, Ready}; @@ -8,16 +9,20 @@ use std::str::FromStr; use thoth_client::Work; use thoth_errors::{ThothError, ThothResult}; -use crate::csv::{CsvSpecification, CsvThoth}; +use crate::csv::{CsvSpecification, CsvThoth, KbartOclc}; use crate::xml::{Onix3Oapen, Onix3ProjectMuse, XmlSpecification}; pub(crate) trait AsRecord {} impl AsRecord for Vec {} +pub const DELIMITER_COMMA: u8 = b','; +pub const DELIMITER_TAB: u8 = b'\t'; + pub(crate) enum MetadataSpecification { Onix3ProjectMuse(Onix3ProjectMuse), Onix3Oapen(Onix3Oapen), CsvThoth(CsvThoth), + KbartOclc(KbartOclc), } pub(crate) struct MetadataRecord { @@ -32,8 +37,10 @@ where { const XML_MIME_TYPE: &'static str = "text/xml; charset=utf-8"; const CSV_MIME_TYPE: &'static str = "text/csv; charset=utf-8"; + const TXT_MIME_TYPE: &'static str = "text/plain; charset=utf-8"; const XML_EXTENSION: &'static str = ".xml"; const CSV_EXTENSION: &'static str = ".csv"; + const TXT_EXTENSION: &'static str = ".txt"; pub(crate) fn new(id: String, specification: MetadataSpecification, data: T) -> Self { MetadataRecord { @@ -48,6 +55,7 @@ where MetadataSpecification::Onix3ProjectMuse(_) => Self::XML_MIME_TYPE, MetadataSpecification::Onix3Oapen(_) => Self::XML_MIME_TYPE, MetadataSpecification::CsvThoth(_) => Self::CSV_MIME_TYPE, + MetadataSpecification::KbartOclc(_) => Self::TXT_MIME_TYPE, } } @@ -56,6 +64,7 @@ where MetadataSpecification::Onix3ProjectMuse(_) => self.xml_file_name(), MetadataSpecification::Onix3Oapen(_) => self.xml_file_name(), MetadataSpecification::CsvThoth(_) => self.csv_file_name(), + MetadataSpecification::KbartOclc(_) => self.txt_file_name(), } } @@ -67,6 +76,10 @@ where self.format_file_name(Self::CSV_EXTENSION) } + fn txt_file_name(&self) -> String { + self.format_file_name(Self::TXT_EXTENSION) + } + fn format_file_name(&self, extension: &'static str) -> String { format!( "{}__{}{}", @@ -88,7 +101,12 @@ impl MetadataRecord> { onix3_project_muse.generate(&self.data) } MetadataSpecification::Onix3Oapen(onix3_oapen) => onix3_oapen.generate(&self.data), - MetadataSpecification::CsvThoth(csv_thoth) => csv_thoth.generate(&self.data), + MetadataSpecification::CsvThoth(csv_thoth) => { + csv_thoth.generate(&self.data, QuoteStyle::Always, DELIMITER_COMMA) + } + MetadataSpecification::KbartOclc(kbart_oclc) => { + kbart_oclc.generate(&self.data, QuoteStyle::Necessary, DELIMITER_TAB) + } } } } @@ -140,6 +158,7 @@ impl FromStr for MetadataSpecification { } "onix_3.0::oapen" => Ok(MetadataSpecification::Onix3Oapen(Onix3Oapen {})), "csv::thoth" => Ok(MetadataSpecification::CsvThoth(CsvThoth {})), + "kbart::oclc" => Ok(MetadataSpecification::KbartOclc(KbartOclc {})), _ => Err(ThothError::InvalidMetadataSpecification(input.to_string())), } } @@ -151,6 +170,7 @@ impl ToString for MetadataSpecification { MetadataSpecification::Onix3ProjectMuse(_) => "onix_3.0::project_muse".to_string(), MetadataSpecification::Onix3Oapen(_) => "onix_3.0::oapen".to_string(), MetadataSpecification::CsvThoth(_) => "csv::thoth".to_string(), + MetadataSpecification::KbartOclc(_) => "kbart::oclc".to_string(), } } } @@ -197,5 +217,11 @@ mod tests { to_test.file_name(), "onix_3.0__oapen__some_id.xml".to_string() ); + let to_test = MetadataRecord::new( + "some_id".to_string(), + MetadataSpecification::KbartOclc(KbartOclc {}), + vec![], + ); + assert_eq!(to_test.file_name(), "kbart__oclc__some_id.txt".to_string()); } }