From dc39d6be3ca13a2b08daf0ace7b1207e3bdaced7 Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Mon, 14 Feb 2022 17:06:22 +0000 Subject: [PATCH 01/27] Add Publication Weight to schema, migrations, API; add conversion function and disambiguate name of existing Length Unit function --- thoth-api/migrations/0.7.3/down.sql | 3 + thoth-api/migrations/0.7.3/up.sql | 4 + thoth-api/src/graphql/model.rs | 35 ++++- thoth-api/src/model/mod.rs | 193 ++++++++++++++++++++---- thoth-api/src/model/publication/crud.rs | 63 +++++++- thoth-api/src/model/publication/mod.rs | 10 ++ thoth-api/src/model/work/crud.rs | 16 +- thoth-api/src/schema.rs | 1 + 8 files changed, 277 insertions(+), 48 deletions(-) create mode 100644 thoth-api/migrations/0.7.3/down.sql create mode 100644 thoth-api/migrations/0.7.3/up.sql diff --git a/thoth-api/migrations/0.7.3/down.sql b/thoth-api/migrations/0.7.3/down.sql new file mode 100644 index 00000000..afdc7054 --- /dev/null +++ b/thoth-api/migrations/0.7.3/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE publication + DROP CONSTRAINT publication_non_physical_no_weight, + DROP COLUMN weight; diff --git a/thoth-api/migrations/0.7.3/up.sql b/thoth-api/migrations/0.7.3/up.sql new file mode 100644 index 00000000..5e3a22d9 --- /dev/null +++ b/thoth-api/migrations/0.7.3/up.sql @@ -0,0 +1,4 @@ +ALTER TABLE publication + ADD COLUMN weight double precision CHECK (weight > 0.0), + ADD CONSTRAINT publication_non_physical_no_weight CHECK + (weight IS NULL OR publication_type = 'Paperback' OR publication_type = 'Hardback'); diff --git a/thoth-api/src/graphql/model.rs b/thoth-api/src/graphql/model.rs index 9f68ccf4..13c311ac 100644 --- a/thoth-api/src/graphql/model.rs +++ b/thoth-api/src/graphql/model.rs @@ -31,6 +31,7 @@ use crate::model::LengthUnit; use crate::model::Orcid; use crate::model::Ror; use crate::model::Timestamp; +use crate::model::WeightUnit; use thoth_errors::{ThothError, ThothResult}; use super::utils::Direction; @@ -1348,7 +1349,11 @@ impl MutationRoot { Contribution::create(&context.db, &data).map_err(|e| e.into()) } - fn create_publication(context: &Context, data: NewPublication) -> FieldResult { + fn create_publication( + context: &Context, + data: NewPublication, + units: WeightUnit, + ) -> FieldResult { context.token.jwt.as_ref().ok_or(ThothError::Unauthorised)?; context .account_access @@ -1358,7 +1363,7 @@ impl MutationRoot { data.can_have_isbn(&context.db)?; } - Publication::create(&context.db, &data).map_err(|e| e.into()) + Publication::create_with_units(&context.db, data, units).map_err(|e| e.into()) } fn create_series(context: &Context, data: NewSeries) -> FieldResult { @@ -1556,7 +1561,11 @@ impl MutationRoot { .map_err(|e| e.into()) } - fn update_publication(context: &Context, data: PatchPublication) -> FieldResult { + fn update_publication( + context: &Context, + data: PatchPublication, + units: WeightUnit, + ) -> FieldResult { context.token.jwt.as_ref().ok_or(ThothError::Unauthorised)?; let publication = Publication::from_id(&context.db, &data.publication_id).unwrap(); context @@ -1575,7 +1584,7 @@ impl MutationRoot { let account_id = context.token.jwt.as_ref().unwrap().account_id(&context.db); publication - .update(&context.db, &data, &account_id) + .update_with_units(&context.db, data, &account_id, units) .map_err(|e| e.into()) } @@ -2032,7 +2041,7 @@ impl Work { )] pub fn width(&self, units: LengthUnit) -> Option { self.width - .map(|w| w.convert_units_from_to(&LengthUnit::Mm, &units)) + .map(|w| w.convert_length_from_to(&LengthUnit::Mm, &units)) } #[graphql( @@ -2046,7 +2055,7 @@ impl Work { )] pub fn height(&self, units: LengthUnit) -> Option { self.height - .map(|h| h.convert_units_from_to(&LengthUnit::Mm, &units)) + .map(|h| h.convert_length_from_to(&LengthUnit::Mm, &units)) } pub fn page_count(&self) -> Option<&i32> { @@ -2477,6 +2486,20 @@ impl Publication { self.updated_at.clone() } + #[graphql( + description = "Weight of the physical Publication (in g or oz) (only applicable to Paperbacks and Hardbacks)", + arguments( + units( + default = WeightUnit::default(), + description = "Unit of measurement in which to represent the weight (grams or ounces)", + ), + ) + )] + pub fn weight(&self, units: WeightUnit) -> Option { + self.weight + .map(|w| w.convert_weight_from_to(&WeightUnit::G, &units)) + } + #[graphql( description = "Get prices linked to this publication", arguments( diff --git a/thoth-api/src/model/mod.rs b/thoth-api/src/model/mod.rs index fb510e51..318b7216 100644 --- a/thoth-api/src/model/mod.rs +++ b/thoth-api/src/model/mod.rs @@ -27,6 +27,19 @@ pub enum LengthUnit { In, } +#[cfg_attr( + feature = "backend", + derive(juniper::GraphQLEnum), + graphql(description = "Unit of measurement for physical Work weight (grams or ounces)") +)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, EnumString, Display)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[strum(serialize_all = "lowercase")] +pub enum WeightUnit { + G, + Oz, +} + #[cfg_attr( feature = "backend", derive(DieselNewType, juniper::GraphQLScalarValue), @@ -81,6 +94,12 @@ impl Default for LengthUnit { } } +impl Default for WeightUnit { + fn default() -> WeightUnit { + WeightUnit::G + } +} + impl Default for Timestamp { fn default() -> Timestamp { Timestamp(TimeZone::timestamp(&Utc, 0, 0)) @@ -463,11 +482,12 @@ macro_rules! db_insert { } pub trait Convert { - fn convert_units_from_to(&self, current_units: &LengthUnit, new_units: &LengthUnit) -> f64; + fn convert_length_from_to(&self, current_units: &LengthUnit, new_units: &LengthUnit) -> f64; + fn convert_weight_from_to(&self, current_units: &WeightUnit, new_units: &WeightUnit) -> f64; } impl Convert for f64 { - fn convert_units_from_to(&self, current_units: &LengthUnit, new_units: &LengthUnit) -> f64 { + fn convert_length_from_to(&self, current_units: &LengthUnit, new_units: &LengthUnit) -> f64 { match (current_units, new_units) { // If current units and new units are the same, no conversion is needed (LengthUnit::Mm, LengthUnit::Mm) @@ -490,6 +510,22 @@ impl Convert for f64 { _ => unimplemented!(), } } + + fn convert_weight_from_to(&self, current_units: &WeightUnit, new_units: &WeightUnit) -> f64 { + match (current_units, new_units) { + // If current units and new units are the same, no conversion is needed + (WeightUnit::G, WeightUnit::G) | (WeightUnit::Oz, WeightUnit::Oz) => *self, + // Return ounce values rounded to 4 decimal places (1 ounce = 28.349523125 grams) + (WeightUnit::G, WeightUnit::Oz) => { + let unrounded_ounces = self / 28.349523125; + // To round to a non-integer scale, multiply by the appropriate factor, + // round to the nearest integer, then divide again by the same factor + (unrounded_ounces * 10000.0).round() / 10000.0 + } + // Return gram values rounded to nearest gram (1 ounce = 28.349523125 grams) + (WeightUnit::Oz, WeightUnit::G) => (self * 28.349523125).round(), + } + } } #[test] @@ -741,74 +777,165 @@ fn test_ror_fromstr() { // Float equality comparison is fine here because the floats // have already been rounded by the functions under test #[allow(clippy::float_cmp)] -fn test_convert_units_from_to() { +fn test_convert_length_from_to() { use LengthUnit::*; - assert_eq!(123.456.convert_units_from_to(&Mm, &Cm), 12.3); - assert_eq!(123.456.convert_units_from_to(&Mm, &In), 4.86); - assert_eq!(123.456.convert_units_from_to(&Cm, &Mm), 1235.0); - assert_eq!(123.456.convert_units_from_to(&In, &Mm), 3136.0); + assert_eq!(123.456.convert_length_from_to(&Mm, &Cm), 12.3); + assert_eq!(123.456.convert_length_from_to(&Mm, &In), 4.86); + assert_eq!(123.456.convert_length_from_to(&Cm, &Mm), 1235.0); + assert_eq!(123.456.convert_length_from_to(&In, &Mm), 3136.0); // Test some standard print sizes - assert_eq!(4.25.convert_units_from_to(&In, &Mm), 108.0); - assert_eq!(108.0.convert_units_from_to(&Mm, &In), 4.25); - assert_eq!(6.0.convert_units_from_to(&In, &Mm), 152.0); - assert_eq!(152.0.convert_units_from_to(&Mm, &In), 5.98); - assert_eq!(8.5.convert_units_from_to(&In, &Mm), 216.0); - assert_eq!(216.0.convert_units_from_to(&Mm, &In), 8.5); + assert_eq!(4.25.convert_length_from_to(&In, &Mm), 108.0); + assert_eq!(108.0.convert_length_from_to(&Mm, &In), 4.25); + assert_eq!(6.0.convert_length_from_to(&In, &Mm), 152.0); + assert_eq!(152.0.convert_length_from_to(&Mm, &In), 5.98); + assert_eq!(8.5.convert_length_from_to(&In, &Mm), 216.0); + assert_eq!(216.0.convert_length_from_to(&Mm, &In), 8.5); // Test that converting and then converting back again // returns a value within a reasonable margin of error assert_eq!( - 5.06.convert_units_from_to(&In, &Mm) - .convert_units_from_to(&Mm, &In), + 5.06.convert_length_from_to(&In, &Mm) + .convert_length_from_to(&Mm, &In), 5.08 ); assert_eq!( - 6.5.convert_units_from_to(&In, &Mm) - .convert_units_from_to(&Mm, &In), + 6.5.convert_length_from_to(&In, &Mm) + .convert_length_from_to(&Mm, &In), 6.5 ); assert_eq!( - 7.44.convert_units_from_to(&In, &Mm) - .convert_units_from_to(&Mm, &In), + 7.44.convert_length_from_to(&In, &Mm) + .convert_length_from_to(&Mm, &In), 7.44 ); assert_eq!( - 8.27.convert_units_from_to(&In, &Mm) - .convert_units_from_to(&Mm, &In), + 8.27.convert_length_from_to(&In, &Mm) + .convert_length_from_to(&Mm, &In), 8.27 ); assert_eq!( - 9.0.convert_units_from_to(&In, &Mm) - .convert_units_from_to(&Mm, &In), + 9.0.convert_length_from_to(&In, &Mm) + .convert_length_from_to(&Mm, &In), 9.02 ); assert_eq!( 10.88 - .convert_units_from_to(&In, &Mm) - .convert_units_from_to(&Mm, &In), + .convert_length_from_to(&In, &Mm) + .convert_length_from_to(&Mm, &In), 10.87 ); assert_eq!( 102.0 - .convert_units_from_to(&Mm, &In) - .convert_units_from_to(&In, &Mm), + .convert_length_from_to(&Mm, &In) + .convert_length_from_to(&In, &Mm), + 102.0 + ); + assert_eq!( + 120.0 + .convert_length_from_to(&Mm, &In) + .convert_length_from_to(&In, &Mm), + 120.0 + ); + assert_eq!( + 168.0 + .convert_length_from_to(&Mm, &In) + .convert_length_from_to(&In, &Mm), + 168.0 + ); + assert_eq!( + 190.0 + .convert_length_from_to(&Mm, &In) + .convert_length_from_to(&In, &Mm), + 190.0 + ); +} + +#[test] +// Float equality comparison is fine here because the floats +// have already been rounded by the functions under test +#[allow(clippy::float_cmp)] +fn test_convert_weight_from_to() { + use WeightUnit::*; + assert_eq!(123.456.convert_weight_from_to(&G, &Oz), 4.3548); + assert_eq!(123.456.convert_weight_from_to(&Oz, &G), 3500.0); + assert_eq!(4.25.convert_weight_from_to(&Oz, &G), 120.0); + assert_eq!(108.0.convert_weight_from_to(&G, &Oz), 3.8096); + assert_eq!(6.0.convert_weight_from_to(&Oz, &G), 170.0); + assert_eq!(152.0.convert_weight_from_to(&G, &Oz), 5.3616); + assert_eq!(8.5.convert_weight_from_to(&Oz, &G), 241.0); + assert_eq!(216.0.convert_weight_from_to(&G, &Oz), 7.6192); + // Test that converting and then converting back again + // returns a value within a reasonable margin of error + assert_eq!( + 5.0.convert_weight_from_to(&Oz, &G) + .convert_weight_from_to(&G, &Oz), + 5.0089 + ); + assert_eq!( + 5.125 + .convert_weight_from_to(&Oz, &G) + .convert_weight_from_to(&G, &Oz), + 5.1147 + ); + assert_eq!( + 6.5.convert_weight_from_to(&Oz, &G) + .convert_weight_from_to(&G, &Oz), + 6.4904 + ); + assert_eq!( + 7.25.convert_weight_from_to(&Oz, &G) + .convert_weight_from_to(&G, &Oz), + 7.2664 + ); + assert_eq!( + 7.44.convert_weight_from_to(&Oz, &G) + .convert_weight_from_to(&G, &Oz), + 7.4428 + ); + assert_eq!( + 8.0625 + .convert_weight_from_to(&Oz, &G) + .convert_weight_from_to(&G, &Oz), + 8.0777 + ); + assert_eq!( + 9.0.convert_weight_from_to(&Oz, &G) + .convert_weight_from_to(&G, &Oz), + 8.9949 + ); + assert_eq!( + 10.75 + .convert_weight_from_to(&Oz, &G) + .convert_weight_from_to(&G, &Oz), + 10.7586 + ); + assert_eq!( + 10.88 + .convert_weight_from_to(&Oz, &G) + .convert_weight_from_to(&G, &Oz), + 10.8644 + ); + assert_eq!( + 102.0 + .convert_weight_from_to(&G, &Oz) + .convert_weight_from_to(&Oz, &G), 102.0 ); assert_eq!( 120.0 - .convert_units_from_to(&Mm, &In) - .convert_units_from_to(&In, &Mm), + .convert_weight_from_to(&G, &Oz) + .convert_weight_from_to(&Oz, &G), 120.0 ); assert_eq!( 168.0 - .convert_units_from_to(&Mm, &In) - .convert_units_from_to(&In, &Mm), + .convert_weight_from_to(&G, &Oz) + .convert_weight_from_to(&Oz, &G), 168.0 ); assert_eq!( 190.0 - .convert_units_from_to(&Mm, &In) - .convert_units_from_to(&In, &Mm), + .convert_weight_from_to(&G, &Oz) + .convert_weight_from_to(&Oz, &G), 190.0 ); } diff --git a/thoth-api/src/model/publication/crud.rs b/thoth-api/src/model/publication/crud.rs index faab1fbb..0c52e564 100644 --- a/thoth-api/src/model/publication/crud.rs +++ b/thoth-api/src/model/publication/crud.rs @@ -3,7 +3,7 @@ use super::{ PublicationHistory, PublicationOrderBy, PublicationType, }; use crate::graphql::utils::Direction; -use crate::model::{Crud, DbInsert, HistoryEntry}; +use crate::model::{Convert, Crud, DbInsert, HistoryEntry, WeightUnit}; use crate::schema::{publication, publication_history}; use crate::{crud_methods, db_insert}; use diesel::dsl::any; @@ -11,6 +11,62 @@ use diesel::{ExpressionMethods, PgTextExpressionMethods, QueryDsl, RunQueryDsl}; use thoth_errors::{ThothError, ThothResult}; use uuid::Uuid; +impl Publication { + pub fn update_with_units( + &self, + db: &crate::db::PgPool, + data: PatchPublication, + account_id: &Uuid, + units: WeightUnit, + ) -> ThothResult { + if units == WeightUnit::G { + // Data is already in units compatible with the database - + // no conversions required before/after updating + self.update(db, &data, account_id) + } else { + let mut converted_data = data; + converted_data.weight = converted_data + .weight + .map(|w| w.convert_weight_from_to(&units, &WeightUnit::G)); + let result = self.update(db, &converted_data, account_id); + if let Ok(mut retrieved_data) = result { + retrieved_data.weight = retrieved_data + .weight + .map(|w| w.convert_weight_from_to(&WeightUnit::G, &units)); + Ok(retrieved_data) + } else { + result + } + } + } + + pub fn create_with_units( + db: &crate::db::PgPool, + data: NewPublication, + units: WeightUnit, + ) -> ThothResult { + if units == WeightUnit::G { + // Data is already in units compatible with the database - + // no conversions required before/after creating + Self::create(db, &data) + } else { + let mut converted_data = data; + converted_data.weight = converted_data + .weight + .map(|w| w.convert_weight_from_to(&units, &WeightUnit::G)); + let result = Self::create(db, &converted_data); + if let Ok(mut retrieved_data) = result { + retrieved_data.weight = retrieved_data + .weight + .map(|w| w.convert_weight_from_to(&WeightUnit::G, &units)); + Ok(retrieved_data) + } else { + result + } + } + } +} + impl Crud for Publication { type NewEntity = NewPublication; type PatchEntity = PatchPublication; @@ -45,6 +101,7 @@ impl Crud for Publication { isbn, created_at, updated_at, + weight, )) .into_boxed(); @@ -73,6 +130,10 @@ impl Crud for Publication { Direction::Asc => query = query.order(updated_at.asc()), Direction::Desc => query = query.order(updated_at.desc()), }, + PublicationField::Weight => match order.direction { + Direction::Asc => query = query.order(weight.asc()), + Direction::Desc => query = query.order(weight.desc()), + }, } if !publishers.is_empty() { query = query.filter(crate::schema::imprint::publisher_id.eq(any(publishers))); diff --git a/thoth-api/src/model/publication/mod.rs b/thoth-api/src/model/publication/mod.rs index c3e7e7e8..73c7608d 100644 --- a/thoth-api/src/model/publication/mod.rs +++ b/thoth-api/src/model/publication/mod.rs @@ -64,6 +64,7 @@ pub enum PublicationField { Isbn, CreatedAt, UpdatedAt, + Weight, } #[cfg_attr(feature = "backend", derive(Queryable))] @@ -76,6 +77,7 @@ pub struct Publication { pub isbn: Option, pub created_at: Timestamp, pub updated_at: Timestamp, + pub weight: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] @@ -86,6 +88,7 @@ pub struct PublicationWithRelations { pub work_id: Uuid, pub isbn: Option, pub updated_at: Timestamp, + pub weight: Option, pub prices: Option>, pub locations: Option>, pub work: WorkWithRelations, @@ -100,6 +103,7 @@ pub struct NewPublication { pub publication_type: PublicationType, pub work_id: Uuid, pub isbn: Option, + pub weight: Option, } #[cfg_attr( @@ -113,6 +117,7 @@ pub struct PatchPublication { pub publication_type: PublicationType, pub work_id: Uuid, pub isbn: Option, + pub weight: Option, } #[cfg_attr(feature = "backend", derive(Queryable))] @@ -192,6 +197,7 @@ fn test_publicationfield_display() { assert_eq!(format!("{}", PublicationField::Isbn), "ISBN"); assert_eq!(format!("{}", PublicationField::CreatedAt), "CreatedAt"); assert_eq!(format!("{}", PublicationField::UpdatedAt), "UpdatedAt"); + assert_eq!(format!("{}", PublicationField::Weight), "Weight"); } #[test] @@ -271,6 +277,10 @@ fn test_publicationfield_fromstr() { PublicationField::from_str("UpdatedAt").unwrap(), PublicationField::UpdatedAt ); + assert_eq!( + PublicationField::from_str("Weight").unwrap(), + PublicationField::Weight + ); assert!(PublicationField::from_str("PublicationID").is_err()); assert!(PublicationField::from_str("Work Title").is_err()); assert!(PublicationField::from_str("Work DOI").is_err()); diff --git a/thoth-api/src/model/work/crud.rs b/thoth-api/src/model/work/crud.rs index 35fd7153..6e00de07 100644 --- a/thoth-api/src/model/work/crud.rs +++ b/thoth-api/src/model/work/crud.rs @@ -94,18 +94,18 @@ impl Work { let mut converted_data = data; converted_data.width = converted_data .width - .map(|w| w.convert_units_from_to(&units, &LengthUnit::Mm)); + .map(|w| w.convert_length_from_to(&units, &LengthUnit::Mm)); converted_data.height = converted_data .height - .map(|h| h.convert_units_from_to(&units, &LengthUnit::Mm)); + .map(|h| h.convert_length_from_to(&units, &LengthUnit::Mm)); let result = self.update(db, &converted_data, account_id); if let Ok(mut retrieved_data) = result { retrieved_data.width = retrieved_data .width - .map(|w| w.convert_units_from_to(&LengthUnit::Mm, &units)); + .map(|w| w.convert_length_from_to(&LengthUnit::Mm, &units)); retrieved_data.height = retrieved_data .height - .map(|h| h.convert_units_from_to(&LengthUnit::Mm, &units)); + .map(|h| h.convert_length_from_to(&LengthUnit::Mm, &units)); Ok(retrieved_data) } else { result @@ -126,18 +126,18 @@ impl Work { let mut converted_data = data; converted_data.width = converted_data .width - .map(|w| w.convert_units_from_to(&units, &LengthUnit::Mm)); + .map(|w| w.convert_length_from_to(&units, &LengthUnit::Mm)); converted_data.height = converted_data .height - .map(|h| h.convert_units_from_to(&units, &LengthUnit::Mm)); + .map(|h| h.convert_length_from_to(&units, &LengthUnit::Mm)); let result = Self::create(db, &converted_data); if let Ok(mut retrieved_data) = result { retrieved_data.width = retrieved_data .width - .map(|w| w.convert_units_from_to(&LengthUnit::Mm, &units)); + .map(|w| w.convert_length_from_to(&LengthUnit::Mm, &units)); retrieved_data.height = retrieved_data .height - .map(|h| h.convert_units_from_to(&LengthUnit::Mm, &units)); + .map(|h| h.convert_length_from_to(&LengthUnit::Mm, &units)); Ok(retrieved_data) } else { result diff --git a/thoth-api/src/schema.rs b/thoth-api/src/schema.rs index f8653a80..306ac97f 100644 --- a/thoth-api/src/schema.rs +++ b/thoth-api/src/schema.rs @@ -301,6 +301,7 @@ table! { isbn -> Nullable, created_at -> Timestamptz, updated_at -> Timestamptz, + weight -> Nullable, } } From b8a225d31f5f3ad8cb1f40bea213984b8d923c41 Mon Sep 17 00:00:00 2001 From: rhigman <73792779+rhigman@users.noreply.github.com> Date: Mon, 14 Feb 2022 17:08:39 +0000 Subject: [PATCH 02/27] Add Publication Weight to app using same logic as lengths [hits Bulma bug when dropdown is duplicated] --- thoth-app/src/component/admin.rs | 91 ++++++++++++--- thoth-app/src/component/new_work.rs | 12 +- thoth-app/src/component/publication.rs | 74 ++++++++++++ thoth-app/src/component/publications_form.rs | 105 ++++++++++++++++++ thoth-app/src/component/utils.rs | 43 +++++++ thoth-app/src/component/work.rs | 39 +++++-- thoth-app/src/lib.rs | 3 +- .../create_publication_mutation.rs | 10 +- thoth-app/src/models/publication/mod.rs | 14 +++ .../models/publication/publication_query.rs | 5 +- .../models/publication/weight_units_query.rs | 33 ++++++ .../src/models/work/create_work_mutation.rs | 2 +- .../src/models/work/update_work_mutation.rs | 2 +- thoth-app/src/models/work/work_query.rs | 12 +- 14 files changed, 400 insertions(+), 45 deletions(-) create mode 100644 thoth-app/src/models/publication/weight_units_query.rs diff --git a/thoth-app/src/component/admin.rs b/thoth-app/src/component/admin.rs index e5f8c5d7..06791459 100644 --- a/thoth-app/src/component/admin.rs +++ b/thoth-app/src/component/admin.rs @@ -1,5 +1,5 @@ use thoth_api::account::model::AccountDetails; -use thoth_api::model::LengthUnit; +use thoth_api::model::{LengthUnit, WeightUnit}; use yew::html; use yew::prelude::*; use yew::services::storage::Area; @@ -43,20 +43,23 @@ use crate::route::AppRoute; use crate::service::account::AccountService; use crate::string::PERMISSIONS_ERROR; use crate::string::STORAGE_ERROR; -use crate::UNITS_KEY; +use crate::LENGTH_UNITS_KEY; +use crate::WEIGHT_UNITS_KEY; pub struct AdminComponent { props: Props, notification_bus: NotificationDispatcher, router: RouteAgentDispatcher<()>, link: ComponentLink, - units_selection: LengthUnit, + length_units_selection: LengthUnit, + weight_units_selection: WeightUnit, previous_route: AdminRoute, } pub enum Msg { RedirectToLogin, UpdateLengthUnit(LengthUnit), + UpdateWeightUnit(WeightUnit), } #[derive(Clone, Properties, PartialEq)] @@ -73,19 +76,33 @@ impl Component for AdminComponent { if !AccountService::new().is_loggedin() { link.send_message(Msg::RedirectToLogin); } - let mut units_selection: LengthUnit = Default::default(); + let mut length_units_selection: LengthUnit = Default::default(); + let mut weight_units_selection: WeightUnit = Default::default(); let previous_route = props.route.clone(); let mut storage_service = StorageService::new(Area::Local).expect(STORAGE_ERROR); - if let Ok(units_string) = storage_service.restore(UNITS_KEY) { - if let Ok(units) = units_string.parse::() { - units_selection = units; + + if let Ok(length_units_string) = storage_service.restore(LENGTH_UNITS_KEY) { + if let Ok(length_units) = length_units_string.parse::() { + length_units_selection = length_units; } else { - // Couldn't parse stored units - overwrite them with default - storage_service.store(UNITS_KEY, Ok(units_selection.to_string())); + // Couldn't parse stored length units - overwrite them with default + storage_service.store(LENGTH_UNITS_KEY, Ok(length_units_selection.to_string())); } } else { - // No stored units found - store the default - storage_service.store(UNITS_KEY, Ok(units_selection.to_string())); + // No stored length units found - store the default + storage_service.store(LENGTH_UNITS_KEY, Ok(length_units_selection.to_string())); + } + + if let Ok(weight_units_string) = storage_service.restore(WEIGHT_UNITS_KEY) { + if let Ok(weight_units) = weight_units_string.parse::() { + weight_units_selection = weight_units; + } else { + // Couldn't parse stored weight units - overwrite them with default + storage_service.store(WEIGHT_UNITS_KEY, Ok(weight_units_selection.to_string())); + } + } else { + // No stored weight units found - store the default + storage_service.store(WEIGHT_UNITS_KEY, Ok(weight_units_selection.to_string())); } AdminComponent { @@ -93,7 +110,8 @@ impl Component for AdminComponent { notification_bus: NotificationBus::dispatcher(), router: RouteAgentDispatcher::new(), link, - units_selection, + length_units_selection, + weight_units_selection, previous_route, } } @@ -125,10 +143,26 @@ impl Component for AdminComponent { false } Msg::UpdateLengthUnit(length_unit) => { - if self.units_selection.neq_assign(length_unit) { + if self.length_units_selection.neq_assign(length_unit) { + StorageService::new(Area::Local) + .expect(STORAGE_ERROR) + .store( + LENGTH_UNITS_KEY, + Ok(self.length_units_selection.to_string()), + ); + true + } else { + false + } + } + Msg::UpdateWeightUnit(weight_unit) => { + if self.weight_units_selection.neq_assign(weight_unit) { StorageService::new(Area::Local) .expect(STORAGE_ERROR) - .store(UNITS_KEY, Ok(self.units_selection.to_string())); + .store( + WEIGHT_UNITS_KEY, + Ok(self.weight_units_selection.to_string()), + ); true } else { false @@ -177,8 +211,24 @@ impl Component for AdminComponent { AdminRoute::Works => html!{}, AdminRoute::Books => html!{}, AdminRoute::Chapters => html!{}, - AdminRoute::Work(id) => html!{}, - AdminRoute::NewWork => html!{}, + AdminRoute::Work(id) => html!{ + + }, + AdminRoute::NewWork => html!{ + + }, AdminRoute::Publishers => html!{}, AdminRoute::Publisher(id) => html!{}, AdminRoute::NewPublisher => html!{}, @@ -189,7 +239,14 @@ impl Component for AdminComponent { AdminRoute::Institution(id) => html!{}, AdminRoute::NewInstitution => html!{}, AdminRoute::Publications => html!{}, - AdminRoute::Publication(id) => html!{}, + AdminRoute::Publication(id) => html!{ + + }, AdminRoute::NewPublication => { html!{
diff --git a/thoth-app/src/component/new_work.rs b/thoth-app/src/component/new_work.rs index 3141efdd..c9ef5d9d 100644 --- a/thoth-app/src/component/new_work.rs +++ b/thoth-app/src/component/new_work.rs @@ -136,8 +136,8 @@ pub enum Msg { #[derive(Clone, Properties)] pub struct Props { pub current_user: AccountDetails, - pub units_selection: LengthUnit, - pub update_units_selection: Callback, + pub length_units_selection: LengthUnit, + pub update_length_units_selection: Callback, pub previous_route: AdminRoute, } @@ -360,7 +360,7 @@ impl Component for NewWorkComponent { cover_url: self.work.cover_url.clone(), cover_caption: self.work.cover_caption.clone(), imprint_id: self.imprint_id, - units: self.props.units_selection.clone(), + units: self.props.length_units_selection.clone(), first_page: self.work.first_page.clone(), last_page: self.work.last_page.clone(), page_interval: self.work.page_interval.clone(), @@ -422,7 +422,7 @@ impl Component for NewWorkComponent { Msg::ChangeWidth(value) => self.work.width.neq_assign(value.to_opt_float()), Msg::ChangeHeight(value) => self.work.height.neq_assign(value.to_opt_float()), Msg::ChangeLengthUnit(length_unit) => { - self.props.update_units_selection.emit(length_unit); + self.props.update_length_units_selection.emit(length_unit); false } Msg::ChangePageCount(value) => self.work.page_count.neq_assign(value.to_opt_int()), @@ -496,7 +496,7 @@ impl Component for NewWorkComponent { }); // Restrict the number of decimal places the user can enter for width/height values // based on currently selected units. - let step = match self.props.units_selection { + let step = match self.props.length_units_selection { LengthUnit::Mm => "1".to_string(), LengthUnit::Cm => "0.1".to_string(), LengthUnit::In => "0.01".to_string(), @@ -657,7 +657,7 @@ impl Component for NewWorkComponent { /> { diff --git a/thoth-app/src/component/publication.rs b/thoth-app/src/component/publication.rs index 1567edf3..5e8e0324 100644 --- a/thoth-app/src/component/publication.rs +++ b/thoth-app/src/component/publication.rs @@ -1,7 +1,9 @@ +use std::str::FromStr; use thoth_api::account::model::AccountDetails; use thoth_api::model::location::Location; use thoth_api::model::price::Price; use thoth_api::model::publication::PublicationWithRelations; +use thoth_api::model::WeightUnit; use uuid::Uuid; use yew::html; use yew::prelude::*; @@ -22,6 +24,7 @@ use crate::agent::notification_bus::Request; use crate::component::delete_dialogue::ConfirmDeleteComponent; use crate::component::locations_form::LocationsFormComponent; use crate::component::prices_form::PricesFormComponent; +use crate::component::utils::FormWeightUnitSelect; use crate::component::utils::Loader; use crate::models::publication::delete_publication_mutation::DeletePublicationRequest; use crate::models::publication::delete_publication_mutation::DeletePublicationRequestBody; @@ -33,28 +36,41 @@ use crate::models::publication::publication_query::FetchPublication; use crate::models::publication::publication_query::PublicationRequest; use crate::models::publication::publication_query::PublicationRequestBody; use crate::models::publication::publication_query::Variables; +use crate::models::publication::weight_units_query::FetchActionWeightUnits; +use crate::models::publication::weight_units_query::FetchWeightUnits; +use crate::models::publication::WeightUnitValues; use crate::route::AdminRoute; use crate::route::AppRoute; use crate::string::RELATIONS_INFO; pub struct PublicationComponent { publication: PublicationWithRelations, + fetch_weight_units: FetchWeightUnits, fetch_publication: FetchPublication, delete_publication: PushDeletePublication, link: ComponentLink, router: RouteAgentDispatcher<()>, notification_bus: NotificationDispatcher, props: Props, + data: PublicationData, +} + +#[derive(Default)] +struct PublicationData { + weight_units: Vec, } #[allow(clippy::large_enum_variant)] pub enum Msg { + SetWeightUnitsFetchState(FetchActionWeightUnits), + GetWeightUnits, SetPublicationFetchState(FetchActionPublication), GetPublication, SetPublicationDeleteState(PushActionDeletePublication), DeletePublication, UpdateLocations(Option>), UpdatePrices(Option>), + ChangeWeightUnit(WeightUnit), ChangeRoute(AppRoute), } @@ -62,6 +78,8 @@ pub enum Msg { pub struct Props { pub publication_id: Uuid, pub current_user: AccountDetails, + pub weight_units_selection: WeightUnit, + pub update_weight_units_selection: Callback, } impl Component for PublicationComponent { @@ -74,22 +92,43 @@ impl Component for PublicationComponent { let notification_bus = NotificationBus::dispatcher(); let publication: PublicationWithRelations = Default::default(); let router = RouteAgentDispatcher::new(); + let data: PublicationData = Default::default(); link.send_message(Msg::GetPublication); + link.send_message(Msg::GetWeightUnits); PublicationComponent { publication, + fetch_weight_units: Default::default(), fetch_publication, delete_publication, link, router, notification_bus, props, + data, } } fn update(&mut self, msg: Self::Message) -> ShouldRender { match msg { + Msg::SetWeightUnitsFetchState(fetch_state) => { + self.fetch_weight_units.apply(fetch_state); + self.data.weight_units = match self.fetch_weight_units.as_ref().state() { + FetchState::NotFetching(_) => vec![], + FetchState::Fetching(_) => vec![], + FetchState::Fetched(body) => body.data.weight_units.enum_values.clone(), + FetchState::Failed(_, _err) => vec![], + }; + true + } + Msg::GetWeightUnits => { + self.link + .send_future(self.fetch_weight_units.fetch(Msg::SetWeightUnitsFetchState)); + self.link + .send_message(Msg::SetWeightUnitsFetchState(FetchAction::Fetching)); + false + } Msg::SetPublicationFetchState(fetch_state) => { self.fetch_publication.apply(fetch_state); match self.fetch_publication.as_ref().state() { @@ -127,6 +166,7 @@ impl Component for PublicationComponent { let body = PublicationRequestBody { variables: Variables { publication_id: Some(self.props.publication_id), + weight_units: self.props.weight_units_selection.clone(), }, ..Default::default() }; @@ -197,6 +237,13 @@ impl Component for PublicationComponent { } Msg::UpdateLocations(locations) => self.publication.locations.neq_assign(locations), Msg::UpdatePrices(prices) => self.publication.prices.neq_assign(prices), + Msg::ChangeWeightUnit(weight_unit) => { + self.props.update_weight_units_selection.emit(weight_unit); + // Callback will prompt parent to update this component's props. + // This will trigger a re-render in change(), so not necessary + // to also re-render here. + false + } Msg::ChangeRoute(r) => { let route = Route::from(r); self.router.send(RouteRequest::ChangeRoute(route)); @@ -206,7 +253,13 @@ impl Component for PublicationComponent { } fn change(&mut self, props: Self::Properties) -> ShouldRender { + let updated_weight_units = + self.props.weight_units_selection != props.weight_units_selection; self.props = props; + if updated_weight_units { + // Required in order to retrieve Weight value in the newly-selected units + self.link.send_message(Msg::GetPublication); + } true } @@ -254,6 +307,27 @@ impl Component for PublicationComponent { +
+ +
+ {&self.publication.weight.as_ref().map(|w| w.to_string()).unwrap_or_else(|| "".to_string())} +
+
+ + { + let value = elem.value(); + Msg::ChangeWeightUnit(WeightUnit::from_str(&value).unwrap()) + } + _ => unreachable!(), + }) + required = true + /> +
diff --git a/thoth-app/src/component/publications_form.rs b/thoth-app/src/component/publications_form.rs index 590eef91..f1cd89e0 100644 --- a/thoth-app/src/component/publications_form.rs +++ b/thoth-app/src/component/publications_form.rs @@ -3,6 +3,7 @@ use thoth_api::model::publication::Publication; use thoth_api::model::publication::PublicationType; use thoth_api::model::work::WorkType; use thoth_api::model::Isbn; +use thoth_api::model::WeightUnit; use thoth_errors::ThothError; use uuid::Uuid; use yew::html; @@ -21,8 +22,10 @@ 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::utils::FormFloatInput; use crate::component::utils::FormPublicationTypeSelect; use crate::component::utils::FormTextInputExtended; +use crate::component::utils::FormWeightUnitSelect; use crate::models::publication::create_publication_mutation::CreatePublicationRequest; use crate::models::publication::create_publication_mutation::CreatePublicationRequestBody; use crate::models::publication::create_publication_mutation::PushActionCreatePublication; @@ -35,7 +38,10 @@ use crate::models::publication::delete_publication_mutation::PushDeletePublicati use crate::models::publication::delete_publication_mutation::Variables as DeleteVariables; use crate::models::publication::publication_types_query::FetchActionPublicationTypes; use crate::models::publication::publication_types_query::FetchPublicationTypes; +use crate::models::publication::weight_units_query::FetchActionWeightUnits; +use crate::models::publication::weight_units_query::FetchWeightUnits; use crate::models::publication::PublicationTypeValues; +use crate::models::publication::WeightUnitValues; use crate::models::EditRoute; use crate::route::AppRoute; use crate::string::CANCEL_BUTTON; @@ -43,6 +49,8 @@ use crate::string::EMPTY_PUBLICATIONS; use crate::string::REMOVE_BUTTON; use crate::string::VIEW_BUTTON; +use super::ToOption; + pub struct PublicationsFormComponent { props: Props, data: PublicationsFormData, @@ -52,6 +60,7 @@ pub struct PublicationsFormComponent { isbn_warning: String, show_add_form: bool, fetch_publication_types: FetchPublicationTypes, + fetch_weight_units: FetchWeightUnits, push_publication: PushCreatePublication, delete_publication: PushDeletePublication, link: ComponentLink, @@ -62,18 +71,23 @@ pub struct PublicationsFormComponent { #[derive(Default)] struct PublicationsFormData { publication_types: Vec, + weight_units: Vec, } pub enum Msg { ToggleAddFormDisplay(bool), SetPublicationTypesFetchState(FetchActionPublicationTypes), GetPublicationTypes, + SetWeightUnitsFetchState(FetchActionWeightUnits), + GetWeightUnits, SetPublicationPushState(PushActionCreatePublication), CreatePublication, SetPublicationDeleteState(PushActionDeletePublication), DeletePublication(Uuid), ChangePublicationType(PublicationType), ChangeIsbn(String), + ChangeWeight(String), + ChangeWeightUnit(WeightUnit), ChangeRoute(AppRoute), } @@ -83,6 +97,8 @@ pub struct Props { pub work_id: Uuid, pub work_type: WorkType, pub update_publications: Callback>>, + pub weight_units_selection: WeightUnit, + pub update_weight_units_selection: Callback, } impl Component for PublicationsFormComponent { @@ -101,6 +117,7 @@ impl Component for PublicationsFormComponent { let router = RouteAgentDispatcher::new(); link.send_message(Msg::GetPublicationTypes); + link.send_message(Msg::GetWeightUnits); PublicationsFormComponent { props, @@ -110,6 +127,7 @@ impl Component for PublicationsFormComponent { isbn_warning, show_add_form, fetch_publication_types: Default::default(), + fetch_weight_units: Default::default(), push_publication, delete_publication, link, @@ -149,6 +167,23 @@ impl Component for PublicationsFormComponent { .send_message(Msg::SetPublicationTypesFetchState(FetchAction::Fetching)); false } + Msg::SetWeightUnitsFetchState(fetch_state) => { + self.fetch_weight_units.apply(fetch_state); + self.data.weight_units = match self.fetch_weight_units.as_ref().state() { + FetchState::NotFetching(_) => vec![], + FetchState::Fetching(_) => vec![], + FetchState::Fetched(body) => body.data.weight_units.enum_values.clone(), + FetchState::Failed(_, _err) => vec![], + }; + true + } + Msg::GetWeightUnits => { + self.link + .send_future(self.fetch_weight_units.fetch(Msg::SetWeightUnitsFetchState)); + self.link + .send_message(Msg::SetWeightUnitsFetchState(FetchAction::Fetching)); + false + } Msg::SetPublicationPushState(fetch_state) => { self.push_publication.apply(fetch_state); match self.push_publication.as_ref().state() { @@ -192,11 +227,20 @@ impl Component for PublicationsFormComponent { } else if let Ok(result) = self.isbn.parse::() { self.new_publication.isbn.neq_assign(Some(result)); } + // Clear any fields which are not applicable to the currently selected publication type. + // (Do not clear them before the save point as the user may change the type again.) + if self.new_publication.publication_type != PublicationType::Paperback + && self.new_publication.publication_type != PublicationType::Hardback + { + self.new_publication.weight = None; + } let body = CreatePublicationRequestBody { variables: Variables { work_id: self.props.work_id, publication_type: self.new_publication.publication_type.clone(), isbn: self.new_publication.isbn.clone(), + weight: self.new_publication.weight, + units: self.props.weight_units_selection.clone(), }, ..Default::default() }; @@ -282,6 +326,16 @@ impl Component for PublicationsFormComponent { false } } + Msg::ChangeWeight(value) => { + self.new_publication.weight.neq_assign(value.to_opt_float()) + } + Msg::ChangeWeightUnit(weight_unit) => { + self.props.update_weight_units_selection.emit(weight_unit); + // Callback will prompt parent to update this component's props. + // This will trigger a re-render in change(), so not necessary + // to also re-render here. + false + } Msg::ChangeRoute(r) => { let route = Route::from(r); self.router.send(RouteRequest::ChangeRoute(route)); @@ -306,6 +360,16 @@ impl Component for PublicationsFormComponent { }); // ISBNs cannot be added for publications whose work type is Book Chapter. let isbn_deactivated = self.props.work_type == WorkType::BookChapter; + // Weight can only be added for physical (Paperback/Hardback) publications. + let weight_deactivated = self.new_publication.publication_type + != PublicationType::Paperback + && self.new_publication.publication_type != PublicationType::Hardback; + // Restrict the number of decimal places the user can enter for weight values + // based on currently selected units. + let step = match self.props.weight_units_selection { + WeightUnit::G => "1".to_string(), + WeightUnit::Oz => "0.0001".to_string(), + }; html! {