diff --git a/CHANGELOG.md b/CHANGELOG.md index 57ec033d..d9a92384 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,13 @@ 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.5.0]](https://github.com/thoth-pub/thoth/releases/tag/v0.5.0) - 2021-11-28 +## [Unreleased] + +## [[0.6.0]](https://github.com/thoth-pub/thoth/releases/tag/v0.6.0) - 2021-11-29 +### Added + - [#92](https://github.com/thoth-pub/thoth/issues/92) - Implement institution table, replacing funder and standardising contributor affiliations + +## [[0.5.0]](https://github.com/thoth-pub/thoth/releases/tag/v0.5.0) - 2021-11-29 ### Added - [#297](https://github.com/thoth-pub/thoth/issues/297) - Implement publication location @@ -14,14 +20,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated if and else branches to comply with [`rustc 1.56.0`](https://github.com/rust-lang/rust/releases/tag/1.56.0) ### Fixed - - [#292](https://github.com/thoth-pub/thoth/issues/292) - Cannot unset pubiication date: error when trying to clear a previously set publication date + - [#292](https://github.com/thoth-pub/thoth/issues/292) - Cannot unset publication date: error when trying to clear a previously set publication date - [#295](https://github.com/thoth-pub/thoth/issues/295) - various subforms failing to trim strings before saving (including on mandatory fields which are checked for emptiness) - Duplicated logic for handling optional field values, simplifying the code and reducing the likelihood of further bugs such as - Minor issue where some required fields were not marked as "required" (so empty values would be sent to the API and raise an error) - Issue with subforms where clicking save button bypassed field requirements (so instead of displaying a warning message such as "Please enter a number", invalid values would be sent to the API and raise an error) - [#310](https://github.com/thoth-pub/thoth/issues/310) - Add jstor specification to formats - ## [[0.4.7]](https://github.com/thoth-pub/thoth/releases/tag/v0.4.7) - 2021-10-04 ### Added - [#43](https://github.com/thoth-pub/thoth/issues/43), [#49](https://github.com/thoth-pub/thoth/issues/49) - Implement EBSCO Host's ONIX 2.1 specification diff --git a/Cargo.lock b/Cargo.lock index 9ddaeecb..9d44ab8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3936,7 +3936,7 @@ dependencies = [ [[package]] name = "thoth" -version = "0.5.0" +version = "0.6.0" dependencies = [ "cargo-husky", "clap", @@ -3951,7 +3951,7 @@ dependencies = [ [[package]] name = "thoth-api" -version = "0.5.0" +version = "0.6.0" dependencies = [ "actix-web", "argon2rs", @@ -3980,7 +3980,7 @@ dependencies = [ [[package]] name = "thoth-api-server" -version = "0.5.0" +version = "0.6.0" dependencies = [ "actix-cors", "actix-identity", @@ -3995,7 +3995,7 @@ dependencies = [ [[package]] name = "thoth-app" -version = "0.5.0" +version = "0.6.0" dependencies = [ "anyhow", "chrono", @@ -4018,7 +4018,7 @@ dependencies = [ [[package]] name = "thoth-app-server" -version = "0.5.0" +version = "0.6.0" dependencies = [ "actix-cors", "actix-web", @@ -4027,7 +4027,7 @@ dependencies = [ [[package]] name = "thoth-client" -version = "0.5.0" +version = "0.6.0" dependencies = [ "chrono", "graphql_client", @@ -4041,7 +4041,7 @@ dependencies = [ [[package]] name = "thoth-errors" -version = "0.5.0" +version = "0.6.0" dependencies = [ "actix-web", "csv", @@ -4056,7 +4056,7 @@ dependencies = [ [[package]] name = "thoth-export-server" -version = "0.5.0" +version = "0.6.0" dependencies = [ "actix-cors", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index 1ff6f735..b0958617 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth" -version = "0.5.0" +version = "0.6.0" 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.5.0", path = "thoth-api", features = ["backend"] } -thoth-api-server = { version = "0.5.0", path = "thoth-api-server" } -thoth-app-server = { version = "0.5.0", path = "thoth-app-server" } -thoth-errors = { version = "0.5.0", path = "thoth-errors" } -thoth-export-server = { version = "0.5.0", path = "thoth-export-server" } +thoth-api = { version = "0.6.0", path = "thoth-api", features = ["backend"] } +thoth-api-server = { version = "0.6.0", path = "thoth-api-server" } +thoth-app-server = { version = "0.6.0", path = "thoth-app-server" } +thoth-errors = { version = "0.6.0", path = "thoth-errors" } +thoth-export-server = { version = "0.6.0", path = "thoth-export-server" } clap = "2.33.3" dialoguer = "0.7.1" dotenv = "0.9.0" diff --git a/diesel.toml b/diesel.toml index 0b29ac61..e8eb9279 100644 --- a/diesel.toml +++ b/diesel.toml @@ -14,4 +14,5 @@ import_types = [ "crate::model::series::Series_type", "crate::model::price::Currency_code", "crate::model::subject::Subject_type" + "crate::model::institution::Country_code" ] diff --git a/thoth-api-server/Cargo.toml b/thoth-api-server/Cargo.toml index 2c1a70d3..ceacafd5 100644 --- a/thoth-api-server/Cargo.toml +++ b/thoth-api-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-api-server" -version = "0.5.0" +version = "0.6.0" 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.5.0", path = "../thoth-api", features = ["backend"] } -thoth-errors = { version = "0.5.0", path = "../thoth-errors" } +thoth-api = { version = "0.6.0", path = "../thoth-api", features = ["backend"] } +thoth-errors = { version = "0.6.0", 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 01fd41ec..85aa8bda 100644 --- a/thoth-api/Cargo.toml +++ b/thoth-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-api" -version = "0.5.0" +version = "0.6.0" 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.5.0", path = "../thoth-errors" } +thoth-errors = { version = "0.6.0", path = "../thoth-errors" } actix-web = { version = "3.3.2", optional = true } argon2rs = "0.2.5" isbn2 = "0.4.0" diff --git a/thoth-api/migrations/0.5.0/up.sql b/thoth-api/migrations/0.5.0/up.sql index 8081eb81..9cbb0c11 100644 --- a/thoth-api/migrations/0.5.0/up.sql +++ b/thoth-api/migrations/0.5.0/up.sql @@ -42,317 +42,11 @@ CREATE TABLE location_history ( timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); --------------------------------------------------------------------------------- ---- START - Data migration for live database. Delete this patch after migration --------------------------------------------------------------------------------- - --- Migrate punctum PDF publications (publisher ID 9c41b13c-cecc-4f6a-a151-be4682915ef5): - --- Each work that has publications should have a canonical PDF publication under a URL beginning "https://cloud.punctumbooks.com/s/". Create a new canonical location for each of these, using the URL as the full_text_url and the work's landing page as the landing_page. - -INSERT INTO location(publication_id, landing_page, full_text_url, canonical) - SELECT publication_id, landing_page, publication_url, True - FROM publication - INNER JOIN work ON publication.work_id = work.work_id - INNER JOIN imprint ON work.imprint_id = imprint.imprint_id - WHERE imprint.publisher_id = '9c41b13c-cecc-4f6a-a151-be4682915ef5' - AND publication.publication_type = 'PDF' - AND publication.publication_url ILIKE 'https://cloud.punctumbooks.com/s/%'; - --- Some works may have additional PDF publications under Project MUSE or JSTOR URLs. Create a new non-canonical location for each of these, using the URL as the landing_page, omitting the full_text_url, and linking to the canonical (punctum cloud) publication. - -INSERT INTO location(publication_id, landing_page, canonical, location_platform) - SELECT a.publication_id, b.publication_url, False, 'Project MUSE' - FROM publication a, publication b - INNER JOIN work ON b.work_id = work.work_id - INNER JOIN imprint ON work.imprint_id = imprint.imprint_id - WHERE imprint.publisher_id = '9c41b13c-cecc-4f6a-a151-be4682915ef5' - AND a.publication_type = 'PDF' - AND a.work_id = b.work_id - AND a.publication_url ILIKE 'https://cloud.punctumbooks.com/s/%' - AND b.publication_url ILIKE 'https://muse.jhu.edu/book/%'; - -INSERT INTO location(publication_id, landing_page, canonical, location_platform) - SELECT a.publication_id, b.publication_url, False, 'JSTOR' - FROM publication a, publication b - INNER JOIN work ON b.work_id = work.work_id - INNER JOIN imprint ON work.imprint_id = imprint.imprint_id - WHERE imprint.publisher_id = '9c41b13c-cecc-4f6a-a151-be4682915ef5' - AND a.publication_type = 'PDF' - AND a.work_id = b.work_id - AND a.publication_url ILIKE 'https://cloud.punctumbooks.com/s/%' - AND b.publication_url ILIKE 'https://www.jstor.org/stable/%'; - --- Some works may have additional PDF publications under OAPEN URLs. Usually, one publication stores the OAPEN publication landing page, and another stores the OAPEN full text link. Create a new non-canonical location for each pair of OAPEN publications, using each URL as either the landing_page or the full_text_url as appropriate, and linking to the canonical (punctum cloud) publication. - -INSERT INTO location(publication_id, landing_page, full_text_url, canonical, location_platform) - SELECT a.publication_id, b.publication_url, c.publication_url, False, 'OAPEN' - FROM publication a, publication b, publication c - INNER JOIN work ON c.work_id = work.work_id - INNER JOIN imprint ON work.imprint_id = imprint.imprint_id - WHERE imprint.publisher_id = '9c41b13c-cecc-4f6a-a151-be4682915ef5' - AND a.publication_type = 'PDF' - AND a.work_id = b.work_id - AND a.work_id = c.work_id - AND a.publication_url ILIKE 'https://cloud.punctumbooks.com/s/%' - AND (b.publication_url ILIKE 'https://library.oapen.org/handle/%' OR b.publication_url ILIKE 'http://library.oapen.org/handle/%') - AND (c.publication_url ILIKE 'https://library.oapen.org/bitstream/%' OR c.publication_url ILIKE 'http://library.oapen.org/bitstream/%'); - --- All MUSE, JSTOR and OAPEN PDF publications should now have had their URL data migrated to location objects. They should not contain any additional (ISBN/price, non-duplicated) data so should be safe to delete. --- In a small number of cases, the OAPEN publications have been misclassified as 'Paperback' rather than 'PDF', so don't restrict the type when deleting. - -DELETE FROM publication USING work, imprint - WHERE publication.work_id = work.work_id - AND work.imprint_id = imprint.imprint_id - AND imprint.publisher_id = '9c41b13c-cecc-4f6a-a151-be4682915ef5' - AND (publication_url ILIKE 'https://muse.jhu.edu/book/%' OR publication_url ILIKE 'https://www.jstor.org/stable/%' OR publication_url ILIKE 'https://library.oapen.org/handle/%' OR publication_url ILIKE 'http://library.oapen.org/handle/%' OR publication_url ILIKE 'https://library.oapen.org/bitstream/%' OR publication_url ILIKE 'http://library.oapen.org/bitstream/%') - AND (isbn IS NULL OR EXISTS ( - SELECT * FROM publication b - WHERE publication.work_id = b.work_id - AND publication.isbn = b.isbn - AND b.publication_url ILIKE 'https://cloud.punctumbooks.com/s/%')) - AND NOT EXISTS (SELECT * FROM price WHERE publication.publication_id = price.publication_id); - --- All canonical (punctum cloud) publications should now have had their URL data migrated to location objects. Their publication_url fields should therefore be safe to clear. - -UPDATE publication SET publication_url = NULL - FROM work, imprint - WHERE publication.work_id = work.work_id - AND work.imprint_id = imprint.imprint_id - AND imprint.publisher_id = '9c41b13c-cecc-4f6a-a151-be4682915ef5' - AND publication_type = 'PDF' - AND publication_url ILIKE 'https://cloud.punctumbooks.com/s/%' - AND EXISTS (SELECT * FROM location WHERE publication.publication_id = location.publication_id AND publication.publication_url = location.full_text_url); - --- Migrate punctum paperback publications (publisher ID 9c41b13c-cecc-4f6a-a151-be4682915ef5): - --- If a work only has one paperback publication, assume that it is the canonical one. Create a new canonical location for each of these, using the URL as the landing_page. - -INSERT INTO location(publication_id, landing_page, canonical) - SELECT publication_id, publication_url, True - FROM publication - INNER JOIN work ON publication.work_id = work.work_id - INNER JOIN imprint ON work.imprint_id = imprint.imprint_id - WHERE imprint.publisher_id = '9c41b13c-cecc-4f6a-a151-be4682915ef5' - AND publication_type = 'Paperback' - AND publication_url IS NOT NULL - AND NOT EXISTS - (SELECT * FROM publication b - WHERE publication.work_id = b.work_id - AND NOT publication.publication_id = b.publication_id - AND b.publication_type = 'Paperback'); - --- Some works have multiple paperback publications. Inspection of the data shows that there are never more than two, they never have more than one distinct ISBN between them, and they never have more than one distinct set of prices between them (although they may have more than one distinct URL). --- Assume that the main publication in these cases is the only one with prices, or else the only one with a URL, or else the one where the URL is a punctumbooks.com landing page (or, if all else is equal, the first one found). Create a canonical location for this publication. - -INSERT INTO location(publication_id, landing_page, canonical) - SELECT a.publication_id, a.publication_url, True - FROM publication a - LEFT JOIN price aprice ON a.publication_id = aprice.publication_id - INNER JOIN work ON a.work_id = work.work_id - INNER JOIN imprint ON work.imprint_id = imprint.imprint_id, - publication b - LEFT JOIN price bprice ON b.publication_id = bprice.publication_id - WHERE imprint.publisher_id = '9c41b13c-cecc-4f6a-a151-be4682915ef5' - AND a.publication_type = 'Paperback' - AND b.publication_type = 'Paperback' - AND a.work_id = b.work_id - AND NOT a.publication_id = b.publication_id - AND a.publication_url IS NOT NULL - AND ((aprice.publication_id IS NOT NULL AND bprice.publication_id IS NULL) - OR ((aprice.currency_code IS NOT DISTINCT FROM bprice.currency_code AND aprice.unit_price IS NOT DISTINCT FROM bprice.unit_price) - AND (b.publication_url IS NULL OR b.publication_url NOT ILIKE 'https://punctumbooks.com/titles/%'))); - --- A single work (ID 98ce9caa-487e-4391-86c9-e5d8129be5b6) has one paperback publication with prices but no URL, and another with a URL but no prices, so it is not covered by the above. Make a canonical location for it manually, attached to the publication with prices, then remove the publication without prices. - -INSERT INTO location(publication_id, landing_page, canonical) - SELECT a.publication_id, b.publication_url, True - FROM publication a, publication b - WHERE a.work_id = '98ce9caa-487e-4391-86c9-e5d8129be5b6' - AND b.work_id = '98ce9caa-487e-4391-86c9-e5d8129be5b6' - AND a.publication_type = 'Paperback' - AND b.publication_type = 'Paperback' - AND NOT a.publication_id = b.publication_id - AND a.publication_url IS NULL - AND b.publication_url IS NOT NULL - AND EXISTS (SELECT * FROM price WHERE price.publication_id = a.publication_id) - AND NOT EXISTS (SELECT * FROM price WHERE price.publication_id = b.publication_id); - -DELETE FROM publication - WHERE work_id = '98ce9caa-487e-4391-86c9-e5d8129be5b6' - AND publication_type = 'Paperback' - AND NOT EXISTS (SELECT * FROM price WHERE price.publication_id = publication_id); - --- Create non-canonical locations under the main publication for all the other URLs associated with this work. - -INSERT INTO location(publication_id, landing_page, canonical) - SELECT a.publication_id, b.publication_url, False - FROM publication a - INNER JOIN work ON a.work_id = work.work_id - INNER JOIN imprint ON work.imprint_id = imprint.imprint_id, - publication b - WHERE imprint.publisher_id = '9c41b13c-cecc-4f6a-a151-be4682915ef5' - AND a.publication_type = 'Paperback' - AND b.publication_type = 'Paperback' - AND a.work_id = b.work_id - AND NOT a.publication_id = b.publication_id - AND EXISTS (SELECT * FROM location WHERE a.publication_id = location.publication_id) - AND b.publication_url IS NOT NULL - AND b.publication_url IS DISTINCT FROM a.publication_url; - --- For any case where the main publication lacks an ISBN, carry over the ISBN (if any) from the other publication. - -UPDATE publication - SET isbn = b.isbn - FROM publication b, work, imprint - WHERE publication.work_id = work.work_id - AND work.imprint_id = imprint.imprint_id - AND imprint.publisher_id = '9c41b13c-cecc-4f6a-a151-be4682915ef5' - AND publication.publication_type = 'Paperback' - AND b.publication_type = 'Paperback' - AND publication.work_id = b.work_id - AND NOT publication.publication_id = b.publication_id - AND EXISTS (SELECT * FROM location WHERE publication.publication_id = location.publication_id) - AND publication.isbn IS NULL; - --- All price, ISBN and URL information in non-main publications should now either be duplicated on the main publication or stored in the location table. Delete these publications. - -DELETE FROM publication USING work, imprint, publication b - WHERE publication.work_id = work.work_id - AND work.imprint_id = imprint.imprint_id - AND imprint.publisher_id = '9c41b13c-cecc-4f6a-a151-be4682915ef5' - AND publication.publication_type = 'Paperback' - AND b.publication_type = 'Paperback' - AND publication.work_id = b.work_id - AND NOT publication.publication_id = b.publication_id - AND NOT EXISTS (SELECT * FROM location WHERE publication.publication_id = location.publication_id) - AND (publication.publication_url IS NULL OR EXISTS (SELECT * FROM location WHERE b.publication_id = location.publication_id AND publication.publication_url = location.landing_page)) - AND (publication.isbn IS NOT DISTINCT FROM b.isbn OR publication.isbn IS NULL) - AND NOT EXISTS (SELECT unit_price, currency_code FROM price WHERE price.publication_id = publication.publication_id EXCEPT SELECT unit_price, currency_code FROM price WHERE price.publication_id = b.publication_id); - --- All remaining publication_urls should now be listed in the location table as the canonical URL for that publication. Remove them from the publications. - -UPDATE publication SET publication_url = NULL - FROM work, imprint - WHERE publication.work_id = work.work_id - AND work.imprint_id = imprint.imprint_id - AND imprint.publisher_id = '9c41b13c-cecc-4f6a-a151-be4682915ef5' - AND publication_type = 'Paperback' - AND publication_url IS NOT NULL - AND EXISTS (SELECT * FROM location WHERE publication.publication_id = location.publication_id AND publication.publication_url = location.landing_page); - --- Migrate remaining duplicate publications: - --- A single meson press work (ID 38872158-58b9-4ddf-a90e-f6001ac6c62d) accounts for all remaining duplicate publications. Inspection of the data shows two PDFs with differing URLs, identical ISBNs and no prices, and three paperbacks with differing URLs, identical ISBNs and two different prices (each in a different currency) between them. Handle these individually. - --- PDFs: one has a meson.press URL, the other an OAPEN URL. Assume that the former is the main one. Create a canonical location for it, create a secondary location for the other one, and then delete the other one and remove the main one's publication_url. - -INSERT INTO location(publication_id, landing_page, full_text_url, canonical) - SELECT publication_id, landing_page, publication_url, True - FROM publication - INNER JOIN work ON publication.work_id = work.work_id - WHERE publication.work_id = '38872158-58b9-4ddf-a90e-f6001ac6c62d' - AND publication.publication_type = 'PDF' - AND publication.publication_url ILIKE 'https://meson.press/wp-content/uploads/%'; - -INSERT INTO location(publication_id, landing_page, canonical, location_platform) - SELECT a.publication_id, b.publication_url, False, 'OAPEN' - FROM publication a, publication b - WHERE a.work_id = '38872158-58b9-4ddf-a90e-f6001ac6c62d' - AND b.work_id = '38872158-58b9-4ddf-a90e-f6001ac6c62d' - AND a.publication_type = 'PDF' - AND b.publication_type = 'PDF' - AND a.publication_url ILIKE 'https://meson.press/wp-content/uploads/%' - AND b.publication_url ILIKE 'https://library.oapen.org/bitstream/%'; - -DELETE FROM publication - WHERE publication.work_id = '38872158-58b9-4ddf-a90e-f6001ac6c62d' - AND publication.publication_type = 'PDF' - AND publication.publication_url ILIKE 'https://library.oapen.org/bitstream/%' - AND (isbn IS NULL OR EXISTS ( - SELECT * FROM publication b - WHERE publication.work_id = b.work_id - AND publication.isbn = b.isbn - AND b.publication_url ILIKE 'https://meson.press/wp-content/uploads/%')) - AND NOT EXISTS (SELECT * FROM price WHERE publication.publication_id = price.publication_id); - -UPDATE publication SET publication_url = NULL - WHERE publication.work_id = '38872158-58b9-4ddf-a90e-f6001ac6c62d' - AND publication.publication_type = 'PDF' - AND publication.publication_url ILIKE 'https://meson.press/wp-content/uploads/%' - AND EXISTS (SELECT * FROM location WHERE publication.publication_id = location.publication_id AND publication.publication_url = location.full_text_url); - --- Paperbacks: none of the URLs are meson.press, so assume that the first publication entered (which has ID 1382662a-ae40-47ae-98a0-34e03ae71366) is the main one. Create a canonical location for it. - -INSERT INTO location(publication_id, landing_page, canonical) - SELECT publication_id, publication_url, True - FROM publication - WHERE publication.publication_id = '1382662a-ae40-47ae-98a0-34e03ae71366'; - --- Create non-canonical locations for the other publications, linked to the main one. - -INSERT INTO location(publication_id, landing_page, canonical) - SELECT '1382662a-ae40-47ae-98a0-34e03ae71366', publication_url, False - FROM publication - WHERE publication.work_id = '38872158-58b9-4ddf-a90e-f6001ac6c62d' - AND publication.publication_type = 'Paperback' - AND NOT publication.publication_id = '1382662a-ae40-47ae-98a0-34e03ae71366'; - --- One of the prices linked to a non-main publication is not duplicated on the main publication. Move it to the main publication. - -UPDATE price SET publication_id = '1382662a-ae40-47ae-98a0-34e03ae71366' - WHERE publication_id = '49003581-5829-457a-b626-a5ab30df9a55'; - --- The non-main paperback publications can now be deleted, and the main publication_url cleared. - -DELETE FROM publication - WHERE publication.work_id = '38872158-58b9-4ddf-a90e-f6001ac6c62d' - AND publication.publication_type = 'Paperback' - AND NOT publication.publication_id = '1382662a-ae40-47ae-98a0-34e03ae71366'; - -UPDATE publication SET publication_url = NULL WHERE publication_id = '1382662a-ae40-47ae-98a0-34e03ae71366'; - --- Migrate all remaining publications: - --- All remaining publications across all publishers should now be unique per work/publication type. Therefore, any URLs which they have can be converted to canonical locations. For hard copy types, convert the publication_url to the location landing_page. For soft copy types, convert the publication_url to the location full_text_url and use the work landing_page as the location landing_page. --- Double-check that no location entry already exists for the publication, and no duplicate publication exists. - +-- Create location entries for every existing publication_url (assume all are landing pages) +-- If a publication has locations, exactly one of them must be canonical; +-- this command will create at most one location per publication, so make them all canonical. INSERT INTO location(publication_id, landing_page, canonical) - SELECT publication_id, publication_url, True - FROM publication - WHERE (publication.publication_type = 'Paperback' OR publication.publication_type = 'Hardback') - AND publication_url IS NOT NULL - AND NOT EXISTS (SELECT * FROM publication b - WHERE publication.work_id = b.work_id - AND NOT publication.publication_id = b.publication_id - AND publication.publication_type = b.publication_type) - AND NOT EXISTS (SELECT * FROM location WHERE publication.publication_id = location.publication_id AND publication.publication_url = location.landing_page); - -INSERT INTO location(publication_id, landing_page, full_text_url, canonical) - SELECT publication_id, landing_page, publication_url, True - FROM publication - INNER JOIN work ON publication.work_id = work.work_id - WHERE (publication.publication_type = 'PDF' OR publication.publication_type = 'Epub' OR publication.publication_type = 'XML' OR publication.publication_type = 'Mobi' OR publication.publication_type = 'HTML') - AND publication_url IS NOT NULL - AND NOT EXISTS (SELECT * FROM publication b - WHERE publication.work_id = b.work_id - AND NOT publication.publication_id = b.publication_id - AND publication.publication_type = b.publication_type) - AND NOT EXISTS (SELECT * FROM location WHERE publication.publication_id = location.publication_id AND publication.publication_url = location.landing_page); - --- All these publications can now have their URLs cleared. - -UPDATE publication SET publication_url = NULL - FROM work - WHERE publication_url IS NOT NULL - AND NOT EXISTS (SELECT * FROM publication b - WHERE publication.work_id = b.work_id - AND NOT publication.publication_id = b.publication_id - AND publication.publication_type = b.publication_type) - AND EXISTS (SELECT * FROM location WHERE publication.publication_id = location.publication_id AND (publication.publication_url = location.landing_page OR publication.publication_url = location.full_text_url)); ------------------------------------------------------------------------------ ---- END - Data migration for live database. Delete this patch after migration ------------------------------------------------------------------------------ + SELECT publication_id, publication_url, True FROM publication WHERE publication_url IS NOT NULL; ALTER TABLE publication -- Only allow one publication of each type per work (existing data may breach this) diff --git a/thoth-api/migrations/0.6.0/down.sql b/thoth-api/migrations/0.6.0/down.sql new file mode 100644 index 00000000..293b9311 --- /dev/null +++ b/thoth-api/migrations/0.6.0/down.sql @@ -0,0 +1,37 @@ +ALTER TABLE contribution + ADD COLUMN institution TEXT CHECK (octet_length(institution) >= 1); + +-- Migrate affiliation information back into contribution table as far as possible +-- before dropping affiliation table. Where a contribution has multiple affiliations, +-- combine the institution names into a single semicolon-separated string. +UPDATE contribution + SET institution = subquery.institutions + FROM ( + SELECT affiliation.contribution_id, string_agg(institution_name, '; ') AS institutions + FROM institution, affiliation + WHERE affiliation.institution_id = institution.institution_id + GROUP BY affiliation.contribution_id + ) AS subquery + WHERE contribution.contribution_id = subquery.contribution_id; + +ALTER TABLE institution_history RENAME COLUMN institution_history_id TO funder_history_id; +ALTER TABLE institution_history RENAME COLUMN institution_id TO funder_id; + +ALTER TABLE institution_history RENAME TO funder_history; + +ALTER TABLE institution RENAME COLUMN institution_id TO funder_id; +ALTER TABLE institution RENAME COLUMN institution_name TO funder_name; +ALTER TABLE institution RENAME COLUMN institution_doi TO funder_doi; + +ALTER TABLE institution + DROP COLUMN ror, + DROP COLUMN country_code; + +ALTER TABLE institution RENAME TO funder; + +ALTER TABLE funding RENAME COLUMN institution_id TO funder_id; + +DROP TYPE IF EXISTS country_code; + +DROP TABLE affiliation_history; +DROP TABLE affiliation; diff --git a/thoth-api/migrations/0.6.0/up.sql b/thoth-api/migrations/0.6.0/up.sql new file mode 100644 index 00000000..925079ce --- /dev/null +++ b/thoth-api/migrations/0.6.0/up.sql @@ -0,0 +1,307 @@ +-- Order is alphabetical by name of country (see string equivalents in API enum) +CREATE TYPE country_code AS ENUM ( + 'afg', + 'ala', + 'alb', + 'dza', + 'asm', + 'and', + 'ago', + 'aia', + 'ata', + 'atg', + 'arg', + 'arm', + 'abw', + 'aus', + 'aut', + 'aze', + 'bhs', + 'bhr', + 'bgd', + 'brb', + 'blr', + 'bel', + 'blz', + 'ben', + 'bmu', + 'btn', + 'bol', + 'bes', + 'bih', + 'bwa', + 'bvt', + 'bra', + 'iot', + 'brn', + 'bgr', + 'bfa', + 'bdi', + 'cpv', + 'khm', + 'cmr', + 'can', + 'cym', + 'caf', + 'tcd', + 'chl', + 'chn', + 'cxr', + 'cck', + 'col', + 'com', + 'cok', + 'cri', + 'civ', + 'hrv', + 'cub', + 'cuw', + 'cyp', + 'cze', + 'cod', + 'dnk', + 'dji', + 'dma', + 'dom', + 'ecu', + 'egy', + 'slv', + 'gnq', + 'eri', + 'est', + 'swz', + 'eth', + 'flk', + 'fro', + 'fji', + 'fin', + 'fra', + 'guf', + 'pyf', + 'atf', + 'gab', + 'gmb', + 'geo', + 'deu', + 'gha', + 'gib', + 'grc', + 'grl', + 'grd', + 'glp', + 'gum', + 'gtm', + 'ggy', + 'gin', + 'gnb', + 'guy', + 'hti', + 'hmd', + 'hnd', + 'hkg', + 'hun', + 'isl', + 'ind', + 'idn', + 'irn', + 'irq', + 'irl', + 'imn', + 'isr', + 'ita', + 'jam', + 'jpn', + 'jey', + 'jor', + 'kaz', + 'ken', + 'kir', + 'kwt', + 'kgz', + 'lao', + 'lva', + 'lbn', + 'lso', + 'lbr', + 'lby', + 'lie', + 'ltu', + 'lux', + 'mac', + 'mdg', + 'mwi', + 'mys', + 'mdv', + 'mli', + 'mlt', + 'mhl', + 'mtq', + 'mrt', + 'mus', + 'myt', + 'mex', + 'fsm', + 'mda', + 'mco', + 'mng', + 'mne', + 'msr', + 'mar', + 'moz', + 'mmr', + 'nam', + 'nru', + 'npl', + 'nld', + 'ncl', + 'nzl', + 'nic', + 'ner', + 'nga', + 'niu', + 'nfk', + 'prk', + 'mkd', + 'mnp', + 'nor', + 'omn', + 'pak', + 'plw', + 'pse', + 'pan', + 'png', + 'pry', + 'per', + 'phl', + 'pcn', + 'pol', + 'prt', + 'pri', + 'qat', + 'cog', + 'reu', + 'rou', + 'rus', + 'rwa', + 'blm', + 'shn', + 'kna', + 'lca', + 'maf', + 'spm', + 'vct', + 'wsm', + 'smr', + 'stp', + 'sau', + 'sen', + 'srb', + 'syc', + 'sle', + 'sgp', + 'sxm', + 'svk', + 'svn', + 'slb', + 'som', + 'zaf', + 'sgs', + 'kor', + 'ssd', + 'esp', + 'lka', + 'sdn', + 'sur', + 'sjm', + 'swe', + 'che', + 'syr', + 'twn', + 'tjk', + 'tza', + 'tha', + 'tls', + 'tgo', + 'tkl', + 'ton', + 'tto', + 'tun', + 'tur', + 'tkm', + 'tca', + 'tuv', + 'uga', + 'ukr', + 'are', + 'gbr', + 'umi', + 'usa', + 'ury', + 'uzb', + 'vut', + 'vat', + 'ven', + 'vnm', + 'vgb', + 'vir', + 'wlf', + 'esh', + 'yem', + 'zmb', + 'zwe' +); + +ALTER TABLE funder RENAME TO institution; + +ALTER TABLE institution RENAME COLUMN funder_id TO institution_id; +ALTER TABLE institution RENAME COLUMN funder_name TO institution_name; +ALTER TABLE institution RENAME COLUMN funder_doi TO institution_doi; + +ALTER TABLE institution + ADD COLUMN ror TEXT CHECK (ror ~ '^https:\/\/ror\.org\/0[a-hjkmnp-z0-9]{6}\d{2}$'), + ADD COLUMN country_code country_code; + +ALTER TABLE funder_history RENAME TO institution_history; + +ALTER TABLE institution_history RENAME COLUMN funder_history_id TO institution_history_id; +ALTER TABLE institution_history RENAME COLUMN funder_id TO institution_id; + +ALTER TABLE funding RENAME COLUMN funder_id TO institution_id; + +CREATE TABLE affiliation ( + affiliation_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + contribution_id UUID NOT NULL REFERENCES contribution(contribution_id) ON DELETE CASCADE, + institution_id UUID NOT NULL REFERENCES institution(institution_id) ON DELETE CASCADE, + affiliation_ordinal INTEGER NOT NULL CHECK (affiliation_ordinal > 0), + position TEXT CHECK (octet_length(position) >= 1), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +SELECT diesel_manage_updated_at('affiliation'); + +-- UNIQ index on affiliation_ordinal and contribution_id +CREATE UNIQUE INDEX affiliation_uniq_ord_in_contribution_idx ON affiliation(contribution_id, affiliation_ordinal); + +CREATE TABLE affiliation_history ( + affiliation_history_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + affiliation_id UUID NOT NULL REFERENCES affiliation(affiliation_id) ON DELETE CASCADE, + account_id UUID NOT NULL REFERENCES account(account_id), + data JSONB NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Create institution entries for every existing contribution institution +-- (unless an institution with that name already exists). +INSERT INTO institution(institution_name) + SELECT DISTINCT institution FROM contribution + WHERE institution IS NOT NULL + AND NOT EXISTS (SELECT * FROM institution WHERE institution_name = contribution.institution); + +-- Create an affiliation linking the appropriate institution to each relevant contribution. +-- (Each contribution will have a maximum of one institution, so all entries can have ordinal 1.) +INSERT INTO affiliation(contribution_id, institution_id, affiliation_ordinal) + SELECT contribution.contribution_id, institution.institution_id, 1 FROM contribution, institution + WHERE contribution.institution = institution.institution_name; + +ALTER TABLE contribution + DROP COLUMN institution; diff --git a/thoth-api/src/graphql/model.rs b/thoth-api/src/graphql/model.rs index 5cfd35ce..5dc69f7e 100644 --- a/thoth-api/src/graphql/model.rs +++ b/thoth-api/src/graphql/model.rs @@ -7,11 +7,12 @@ use uuid::Uuid; use crate::account::model::AccountAccess; use crate::account::model::DecodedToken; use crate::db::PgPool; +use crate::model::affiliation::*; use crate::model::contribution::*; use crate::model::contributor::*; -use crate::model::funder::*; use crate::model::funding::*; use crate::model::imprint::*; +use crate::model::institution::*; use crate::model::issue::*; use crate::model::language::*; use crate::model::location::*; @@ -27,6 +28,7 @@ use crate::model::Doi; use crate::model::Isbn; use crate::model::LengthUnit; use crate::model::Orcid; +use crate::model::Ror; use crate::model::Timestamp; use thoth_errors::{ThothError, ThothResult}; @@ -899,28 +901,28 @@ impl QueryRoot { } #[graphql( - description = "Query the full list of funders", + description = "Query the full list of institutions", arguments( limit(default = 100, description = "The number of items to return"), 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 funderName and funderDoi", + 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 institution_name, ror and institution_doi", ), order( - default = FunderOrderBy::default(), + default = InstitutionOrderBy::default(), description = "The order in which to sort the results", ), ) )] - fn funders( + fn institutions( context: &Context, limit: i32, offset: i32, filter: String, - order: FunderOrderBy, - ) -> FieldResult> { - Funder::all( + order: InstitutionOrderBy, + ) -> FieldResult> { + Institution::all( &context.db, limit, offset, @@ -935,22 +937,22 @@ impl QueryRoot { .map_err(|e| e.into()) } - #[graphql(description = "Query a single funder using its id")] - fn funder(context: &Context, funder_id: Uuid) -> FieldResult { - Funder::from_id(&context.db, &funder_id).map_err(|e| e.into()) + #[graphql(description = "Query a single institution using its id")] + fn institution(context: &Context, institution_id: Uuid) -> FieldResult { + Institution::from_id(&context.db, &institution_id).map_err(|e| e.into()) } #[graphql( - description = "Get the total number of funders", + description = "Get the total number of institutions", 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 funderName and funderDoi", + 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 institution_name, ror and institution_doi", ), ) )] - fn funder_count(context: &Context, filter: String) -> FieldResult { - Funder::count(&context.db, Some(filter), vec![], None, None).map_err(|e| e.into()) + fn institution_count(context: &Context, filter: String) -> FieldResult { + Institution::count(&context.db, Some(filter), vec![], None, None).map_err(|e| e.into()) } #[graphql( @@ -1004,6 +1006,58 @@ impl QueryRoot { fn funding_count(context: &Context) -> FieldResult { Funding::count(&context.db, None, vec![], None, None).map_err(|e| e.into()) } + + #[graphql( + description = "Query the full list of affiliations", + arguments( + limit(default = 100, description = "The number of items to return"), + offset(default = 0, description = "The number of items to skip"), + order( + default = { + AffiliationOrderBy { + field: AffiliationField::AffiliationOrdinal, + direction: Direction::Asc, + } + }, + description = "The order in which to sort the results", + ), + publishers( + default = vec![], + description = "If set, only shows results connected to publishers with these IDs", + ), + ) + )] + fn affiliations( + context: &Context, + limit: i32, + offset: i32, + order: AffiliationOrderBy, + publishers: Vec, + ) -> FieldResult> { + Affiliation::all( + &context.db, + limit, + offset, + None, + order, + publishers, + None, + None, + None, + None, + ) + .map_err(|e| e.into()) + } + + #[graphql(description = "Query a single affiliation using its id")] + fn affiliation(context: &Context, affiliation_id: Uuid) -> FieldResult { + Affiliation::from_id(&context.db, &affiliation_id).map_err(|e| e.into()) + } + + #[graphql(description = "Get the total number of affiliations")] + fn affiliation_count(context: &Context) -> FieldResult { + Affiliation::count(&context.db, None, vec![], None, None).map_err(|e| e.into()) + } } pub struct MutationRoot; @@ -1092,9 +1146,9 @@ impl MutationRoot { Language::create(&context.db, &data).map_err(|e| e.into()) } - fn create_funder(context: &Context, data: NewFunder) -> FieldResult { + fn create_institution(context: &Context, data: NewInstitution) -> FieldResult { context.token.jwt.as_ref().ok_or(ThothError::Unauthorised)?; - Funder::create(&context.db, &data).map_err(|e| e.into()) + Institution::create(&context.db, &data).map_err(|e| e.into()) } fn create_funding(context: &Context, data: NewFunding) -> FieldResult { @@ -1147,6 +1201,18 @@ impl MutationRoot { Subject::create(&context.db, &data).map_err(|e| e.into()) } + fn create_affiliation(context: &Context, data: NewAffiliation) -> FieldResult { + context.token.jwt.as_ref().ok_or(ThothError::Unauthorised)?; + context + .account_access + .can_edit(publisher_id_from_contribution_id( + &context.db, + data.contribution_id, + )?)?; + + Affiliation::create(&context.db, &data).map_err(|e| e.into()) + } + fn update_work(context: &Context, data: PatchWork, units: LengthUnit) -> FieldResult { context.token.jwt.as_ref().ok_or(ThothError::Unauthorised)?; let work = Work::from_id(&context.db, &data.work_id).unwrap(); @@ -1310,10 +1376,10 @@ impl MutationRoot { .map_err(|e| e.into()) } - fn update_funder(context: &Context, data: PatchFunder) -> FieldResult { + fn update_institution(context: &Context, data: PatchInstitution) -> FieldResult { context.token.jwt.as_ref().ok_or(ThothError::Unauthorised)?; let account_id = context.token.jwt.as_ref().unwrap().account_id(&context.db); - Funder::from_id(&context.db, &data.funder_id) + Institution::from_id(&context.db, &data.institution_id) .unwrap() .update(&context.db, &data, &account_id) .map_err(|e| e.into()) @@ -1414,6 +1480,28 @@ impl MutationRoot { .map_err(|e| e.into()) } + fn update_affiliation(context: &Context, data: PatchAffiliation) -> FieldResult { + context.token.jwt.as_ref().ok_or(ThothError::Unauthorised)?; + let affiliation = Affiliation::from_id(&context.db, &data.affiliation_id).unwrap(); + context + .account_access + .can_edit(affiliation.publisher_id(&context.db)?)?; + + if !(data.contribution_id == affiliation.contribution_id) { + context + .account_access + .can_edit(publisher_id_from_contribution_id( + &context.db, + data.contribution_id, + )?)?; + } + + let account_id = context.token.jwt.as_ref().unwrap().account_id(&context.db); + affiliation + .update(&context.db, &data, &account_id) + .map_err(|e| e.into()) + } + fn delete_work(context: &Context, work_id: Uuid) -> FieldResult { context.token.jwt.as_ref().ok_or(ThothError::Unauthorised)?; let work = Work::from_id(&context.db, &work_id).unwrap(); @@ -1500,9 +1588,9 @@ impl MutationRoot { language.delete(&context.db).map_err(|e| e.into()) } - fn delete_funder(context: &Context, funder_id: Uuid) -> FieldResult { + fn delete_institution(context: &Context, institution_id: Uuid) -> FieldResult { context.token.jwt.as_ref().ok_or(ThothError::Unauthorised)?; - Funder::from_id(&context.db, &funder_id) + Institution::from_id(&context.db, &institution_id) .unwrap() .delete(&context.db) .map_err(|e| e.into()) @@ -1547,6 +1635,16 @@ impl MutationRoot { subject.delete(&context.db).map_err(|e| e.into()) } + + fn delete_affiliation(context: &Context, affiliation_id: Uuid) -> FieldResult { + context.token.jwt.as_ref().ok_or(ThothError::Unauthorised)?; + let affiliation = Affiliation::from_id(&context.db, &affiliation_id).unwrap(); + context + .account_access + .can_edit(affiliation.publisher_id(&context.db)?)?; + + affiliation.delete(&context.db).map_err(|e| e.into()) + } } #[juniper::object(Context = Context, description = "A written text that can be published")] @@ -2328,10 +2426,6 @@ impl Contribution { self.biography.as_ref() } - pub fn institution(&self) -> Option<&String> { - self.institution.as_ref() - } - pub fn created_at(&self) -> Timestamp { self.created_at.clone() } @@ -2363,6 +2457,44 @@ impl Contribution { pub fn contributor(&self, context: &Context) -> FieldResult { Contributor::from_id(&context.db, &self.contributor_id).map_err(|e| e.into()) } + + #[graphql( + description = "Get affiliations linked to this contribution", + arguments( + limit(default = 100, description = "The number of items to return"), + offset(default = 0, description = "The number of items to skip"), + order( + default = { + AffiliationOrderBy { + field: AffiliationField::AffiliationOrdinal, + direction: Direction::Asc, + } + }, + description = "The order in which to sort the results", + ), + ) + )] + pub fn affiliations( + &self, + context: &Context, + limit: i32, + offset: i32, + order: AffiliationOrderBy, + ) -> FieldResult> { + Affiliation::all( + &context.db, + limit, + offset, + None, + order, + vec![], + None, + Some(self.contribution_id), + None, + None, + ) + .map_err(|e| e.into()) + } } #[juniper::object(Context = Context, description = "A periodical of publications about a particular subject.")] @@ -2621,18 +2753,32 @@ impl Subject { } } -#[juniper::object(Context = Context, description = "An organisation that provides the money to pay for the publication of a work.")] -impl Funder { - pub fn funder_id(&self) -> &Uuid { - &self.funder_id +#[juniper::object(Context = Context, description = "An organisation with which contributors may be affiliated or by which works may be funded.")] +impl Institution { + pub fn institution_id(&self) -> &Uuid { + &self.institution_id + } + + pub fn institution_name(&self) -> &String { + &self.institution_name + } + + #[graphql( + description = "Digital Object Identifier of the organisation as full URL. It must use the HTTPS scheme and the doi.org domain (e.g. https://doi.org/10.13039/100014013)" + )] + pub fn institution_doi(&self) -> Option<&Doi> { + self.institution_doi.as_ref() } - pub fn funder_name(&self) -> &String { - &self.funder_name + pub fn country_code(&self) -> Option<&CountryCode> { + self.country_code.as_ref() } - pub fn funder_doi(&self) -> Option<&Doi> { - self.funder_doi.as_ref() + #[graphql( + description = "Research Organisation Registry identifier of the organisation as full URL. It must use the HTTPS scheme and the ror.org domain (e.g. https://ror.org/051z6e826)" + )] + pub fn ror(&self) -> Option<&Ror> { + self.ror.as_ref() } pub fn created_at(&self) -> Timestamp { @@ -2644,7 +2790,7 @@ impl Funder { } #[graphql( - description = "Get fundings linked to this funder", + description = "Get fundings linked to this institution", arguments( limit(default = 100, description = "The number of items to return"), offset(default = 0, description = "The number of items to skip"), @@ -2674,7 +2820,45 @@ impl Funder { order, vec![], None, - Some(self.funder_id), + Some(self.institution_id), + None, + None, + ) + .map_err(|e| e.into()) + } + + #[graphql( + description = "Get affiliations linked to this institution", + arguments( + limit(default = 100, description = "The number of items to return"), + offset(default = 0, description = "The number of items to skip"), + order( + default = { + AffiliationOrderBy { + field: AffiliationField::AffiliationOrdinal, + direction: Direction::Asc, + } + }, + description = "The order in which to sort the results", + ), + ) + )] + pub fn affiliations( + &self, + context: &Context, + limit: i32, + offset: i32, + order: AffiliationOrderBy, + ) -> FieldResult> { + Affiliation::all( + &context.db, + limit, + offset, + None, + order, + vec![], + Some(self.institution_id), + None, None, None, ) @@ -2682,7 +2866,7 @@ impl Funder { } } -#[juniper::object(Context = Context, description = "A grant awarded to the publication of a work by a funder.")] +#[juniper::object(Context = Context, description = "A grant awarded to the publication of a work by an institution.")] impl Funding { pub fn funding_id(&self) -> &Uuid { &self.funding_id @@ -2692,8 +2876,8 @@ impl Funding { &self.work_id } - pub fn funder_id(&self) -> &Uuid { - &self.funder_id + pub fn institution_id(&self) -> &Uuid { + &self.institution_id } pub fn program(&self) -> Option<&String> { @@ -2728,8 +2912,47 @@ impl Funding { Work::from_id(&context.db, &self.work_id).map_err(|e| e.into()) } - pub fn funder(&self, context: &Context) -> FieldResult { - Funder::from_id(&context.db, &self.funder_id).map_err(|e| e.into()) + pub fn institution(&self, context: &Context) -> FieldResult { + Institution::from_id(&context.db, &self.institution_id).map_err(|e| e.into()) + } +} + +#[juniper::object(Context = Context, description = "An association between a person and an institution for a specific contribution.")] +impl Affiliation { + pub fn affiliation_id(&self) -> Uuid { + self.affiliation_id + } + + pub fn contribution_id(&self) -> Uuid { + self.contribution_id + } + + pub fn institution_id(&self) -> Uuid { + self.institution_id + } + + pub fn affiliation_ordinal(&self) -> &i32 { + &self.affiliation_ordinal + } + + pub fn position(&self) -> Option<&String> { + self.position.as_ref() + } + + pub fn created_at(&self) -> Timestamp { + self.created_at.clone() + } + + pub fn updated_at(&self) -> Timestamp { + self.updated_at.clone() + } + + pub fn institution(&self, context: &Context) -> FieldResult { + Institution::from_id(&context.db, &self.institution_id).map_err(|e| e.into()) + } + + pub fn contribution(&self, context: &Context) -> FieldResult { + Contribution::from_id(&context.db, &self.contribution_id).map_err(|e| e.into()) } } @@ -2753,3 +2976,10 @@ fn publisher_id_from_publication_id( ) -> ThothResult { Publication::from_id(db, &publication_id)?.publisher_id(db) } + +fn publisher_id_from_contribution_id( + db: &crate::db::PgPool, + contribution_id: Uuid, +) -> ThothResult { + Contribution::from_id(db, &contribution_id)?.publisher_id(db) +} diff --git a/thoth-api/src/model/affiliation/crud.rs b/thoth-api/src/model/affiliation/crud.rs new file mode 100644 index 00000000..98031fbf --- /dev/null +++ b/thoth-api/src/model/affiliation/crud.rs @@ -0,0 +1,177 @@ +use super::{ + Affiliation, AffiliationField, AffiliationHistory, AffiliationOrderBy, NewAffiliation, + NewAffiliationHistory, PatchAffiliation, +}; +use crate::graphql::utils::Direction; +use crate::model::{Crud, DbInsert, HistoryEntry}; +use crate::schema::{affiliation, affiliation_history}; +use crate::{crud_methods, db_insert}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; +use thoth_errors::{ThothError, ThothResult}; +use uuid::Uuid; + +impl Crud for Affiliation { + type NewEntity = NewAffiliation; + type PatchEntity = PatchAffiliation; + type OrderByEntity = AffiliationOrderBy; + type FilterParameter1 = (); + type FilterParameter2 = (); + + fn pk(&self) -> Uuid { + self.affiliation_id + } + + fn all( + db: &crate::db::PgPool, + limit: i32, + offset: i32, + _: Option, + order: Self::OrderByEntity, + publishers: Vec, + parent_id_1: Option, + parent_id_2: Option, + _: Option, + _: Option, + ) -> ThothResult> { + use crate::schema::affiliation::dsl::*; + let connection = db.get().unwrap(); + let mut query = + affiliation + .inner_join(crate::schema::contribution::table.inner_join( + crate::schema::work::table.inner_join(crate::schema::imprint::table), + )) + .select(( + affiliation_id, + contribution_id, + institution_id, + affiliation_ordinal, + position, + created_at, + updated_at, + )) + .into_boxed(); + + match order.field { + AffiliationField::AffiliationId => match order.direction { + Direction::Asc => query = query.order(affiliation_id.asc()), + Direction::Desc => query = query.order(affiliation_id.desc()), + }, + AffiliationField::ContributionId => match order.direction { + Direction::Asc => query = query.order(contribution_id.asc()), + Direction::Desc => query = query.order(contribution_id.desc()), + }, + AffiliationField::InstitutionId => match order.direction { + Direction::Asc => query = query.order(institution_id.asc()), + Direction::Desc => query = query.order(institution_id.desc()), + }, + AffiliationField::AffiliationOrdinal => match order.direction { + Direction::Asc => query = query.order(affiliation_ordinal.asc()), + Direction::Desc => query = query.order(affiliation_ordinal.desc()), + }, + AffiliationField::Position => match order.direction { + Direction::Asc => query = query.order(position.asc()), + Direction::Desc => query = query.order(position.desc()), + }, + AffiliationField::CreatedAt => match order.direction { + Direction::Asc => query = query.order(created_at.asc()), + Direction::Desc => query = query.order(created_at.desc()), + }, + AffiliationField::UpdatedAt => match order.direction { + Direction::Asc => query = query.order(updated_at.asc()), + Direction::Desc => query = query.order(updated_at.desc()), + }, + } + // This loop must appear before any other filter statements, as it takes advantage of + // the behaviour of `or_filter` being equal to `filter` when no other filters are present yet. + // Result needs to be `WHERE (x = $1 [OR x = $2...]) AND ([...])` - note bracketing. + for pub_id in publishers { + query = query.or_filter(crate::schema::imprint::publisher_id.eq(pub_id)); + } + if let Some(pid) = parent_id_1 { + query = query.filter(institution_id.eq(pid)); + } + if let Some(pid) = parent_id_2 { + query = query.filter(contribution_id.eq(pid)); + } + match query + .limit(limit.into()) + .offset(offset.into()) + .load::(&connection) + { + Ok(t) => Ok(t), + Err(e) => Err(ThothError::from(e)), + } + } + + fn count( + db: &crate::db::PgPool, + _: Option, + _: Vec, + _: Option, + _: Option, + ) -> ThothResult { + use crate::schema::affiliation::dsl::*; + let connection = db.get().unwrap(); + + // `SELECT COUNT(*)` in postgres returns a BIGINT, which diesel parses as i64. Juniper does + // not implement i64 yet, only i32. The only sensible way, albeit shameful, to solve this + // is converting i64 to string and then parsing it as i32. This should institution until we reach + // 2147483647 records - if you are fixing this bug, congratulations on book number 2147483647! + match affiliation.count().get_result::(&connection) { + Ok(t) => Ok(t.to_string().parse::().unwrap()), + Err(e) => Err(ThothError::from(e)), + } + } + + fn publisher_id(&self, db: &crate::db::PgPool) -> ThothResult { + crate::model::contribution::Contribution::from_id(db, &self.contribution_id)? + .publisher_id(db) + } + + crud_methods!(affiliation::table, affiliation::dsl::affiliation); +} + +impl HistoryEntry for Affiliation { + type NewHistoryEntity = NewAffiliationHistory; + + fn new_history_entry(&self, account_id: &Uuid) -> Self::NewHistoryEntity { + Self::NewHistoryEntity { + affiliation_id: self.affiliation_id, + account_id: *account_id, + data: serde_json::Value::String(serde_json::to_string(&self).unwrap()), + } + } +} + +impl DbInsert for NewAffiliationHistory { + type MainEntity = AffiliationHistory; + + db_insert!(affiliation_history::table); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_affiliation_pk() { + let affiliation: Affiliation = Default::default(); + assert_eq!(affiliation.pk(), affiliation.affiliation_id); + } + + #[test] + fn test_new_affiliation_history_from_affiliation() { + let affiliation: Affiliation = Default::default(); + let account_id: Uuid = Default::default(); + let new_affiliation_history = affiliation.new_history_entry(&account_id); + assert_eq!( + new_affiliation_history.affiliation_id, + affiliation.affiliation_id + ); + assert_eq!(new_affiliation_history.account_id, account_id); + assert_eq!( + new_affiliation_history.data, + serde_json::Value::String(serde_json::to_string(&affiliation).unwrap()) + ); + } +} diff --git a/thoth-api/src/model/affiliation/mod.rs b/thoth-api/src/model/affiliation/mod.rs new file mode 100644 index 00000000..a040378d --- /dev/null +++ b/thoth-api/src/model/affiliation/mod.rs @@ -0,0 +1,128 @@ +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::graphql::utils::Direction; +use crate::model::contribution::ContributionWithWork; +use crate::model::institution::Institution; +use crate::model::Timestamp; +#[cfg(feature = "backend")] +use crate::schema::affiliation; +#[cfg(feature = "backend")] +use crate::schema::affiliation_history; + +#[cfg_attr( + feature = "backend", + derive(juniper::GraphQLEnum), + graphql(description = "Field to use when sorting affiliations list") +)] +pub enum AffiliationField { + AffiliationId, + ContributionId, + InstitutionId, + AffiliationOrdinal, + Position, + CreatedAt, + UpdatedAt, +} + +#[cfg_attr(feature = "backend", derive(Queryable))] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Affiliation { + pub affiliation_id: Uuid, + pub contribution_id: Uuid, + pub institution_id: Uuid, + pub affiliation_ordinal: i32, + pub position: Option, + pub created_at: Timestamp, + pub updated_at: Timestamp, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct AffiliationWithInstitution { + pub affiliation_id: Uuid, + pub contribution_id: Uuid, + pub institution_id: Uuid, + pub affiliation_ordinal: i32, + pub position: Option, + pub institution: Institution, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct AffiliationWithContribution { + pub contribution: ContributionWithWork, +} + +#[cfg_attr( + feature = "backend", + derive(juniper::GraphQLInputObject, Insertable), + table_name = "affiliation" +)] +pub struct NewAffiliation { + pub contribution_id: Uuid, + pub institution_id: Uuid, + pub affiliation_ordinal: i32, + pub position: Option, +} + +#[cfg_attr( + feature = "backend", + derive(juniper::GraphQLInputObject, AsChangeset), + changeset_options(treat_none_as_null = "true"), + table_name = "affiliation" +)] +pub struct PatchAffiliation { + pub affiliation_id: Uuid, + pub contribution_id: Uuid, + pub institution_id: Uuid, + pub affiliation_ordinal: i32, + pub position: Option, +} + +#[cfg_attr(feature = "backend", derive(Queryable))] +pub struct AffiliationHistory { + pub affiliation_history_id: Uuid, + pub affiliation_id: Uuid, + pub account_id: Uuid, + pub data: serde_json::Value, + pub timestamp: Timestamp, +} + +#[cfg_attr( + feature = "backend", + derive(Insertable), + table_name = "affiliation_history" +)] +pub struct NewAffiliationHistory { + pub affiliation_id: Uuid, + pub account_id: Uuid, + pub data: serde_json::Value, +} + +#[cfg_attr( + feature = "backend", + derive(juniper::GraphQLInputObject), + graphql(description = "Field and order to use when sorting affiliations list") +)] +pub struct AffiliationOrderBy { + pub field: AffiliationField, + pub direction: Direction, +} + +impl Default for AffiliationWithInstitution { + fn default() -> AffiliationWithInstitution { + AffiliationWithInstitution { + affiliation_id: Default::default(), + institution_id: Default::default(), + contribution_id: Default::default(), + affiliation_ordinal: 1, + position: Default::default(), + institution: Default::default(), + } + } +} + +#[cfg(feature = "backend")] +pub mod crud; diff --git a/thoth-api/src/model/contribution/crud.rs b/thoth-api/src/model/contribution/crud.rs index c5a95b00..db21b084 100644 --- a/thoth-api/src/model/contribution/crud.rs +++ b/thoth-api/src/model/contribution/crud.rs @@ -45,7 +45,6 @@ impl Crud for Contribution { dsl::contribution_type, dsl::main_contribution, dsl::biography, - dsl::institution, dsl::created_at, dsl::updated_at, dsl::first_name, @@ -80,10 +79,6 @@ impl Crud for Contribution { Direction::Asc => query = query.order(dsl::biography.asc()), Direction::Desc => query = query.order(dsl::biography.desc()), }, - ContributionField::Institution => match order.direction { - Direction::Asc => query = query.order(dsl::institution.asc()), - Direction::Desc => query = query.order(dsl::institution.desc()), - }, ContributionField::CreatedAt => match order.direction { Direction::Asc => query = query.order(dsl::created_at.asc()), Direction::Desc => query = query.order(dsl::created_at.desc()), diff --git a/thoth-api/src/model/contribution/mod.rs b/thoth-api/src/model/contribution/mod.rs index 805153d3..685bb3a3 100644 --- a/thoth-api/src/model/contribution/mod.rs +++ b/thoth-api/src/model/contribution/mod.rs @@ -3,6 +3,7 @@ use strum::Display; use strum::EnumString; use uuid::Uuid; +use crate::model::affiliation::AffiliationWithInstitution; use crate::model::work::WorkWithRelations; use crate::model::Timestamp; #[cfg(feature = "backend")] @@ -45,7 +46,6 @@ pub enum ContributionField { ContributionType, MainContribution, Biography, - Institution, CreatedAt, UpdatedAt, FirstName, @@ -64,7 +64,6 @@ pub struct Contribution { pub contribution_type: ContributionType, pub main_contribution: bool, pub biography: Option, - pub institution: Option, pub created_at: Timestamp, pub updated_at: Timestamp, pub first_name: Option, @@ -73,6 +72,12 @@ pub struct Contribution { pub contribution_ordinal: i32, } +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ContributionWithAffiliations { + pub affiliations: Option>, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ContributionWithWork { @@ -90,7 +95,6 @@ pub struct NewContribution { pub contribution_type: ContributionType, pub main_contribution: bool, pub biography: Option, - pub institution: Option, pub first_name: Option, pub last_name: String, pub full_name: String, @@ -110,7 +114,6 @@ pub struct PatchContribution { pub contribution_type: ContributionType, pub main_contribution: bool, pub biography: Option, - pub institution: Option, pub first_name: Option, pub last_name: String, pub full_name: String, @@ -152,7 +155,6 @@ impl Default for Contribution { contribution_type: Default::default(), main_contribution: Default::default(), biography: Default::default(), - institution: Default::default(), created_at: Default::default(), updated_at: Default::default(), first_name: Default::default(), diff --git a/thoth-api/src/model/funder/crud.rs b/thoth-api/src/model/funder/crud.rs deleted file mode 100644 index a6fbe085..00000000 --- a/thoth-api/src/model/funder/crud.rs +++ /dev/null @@ -1,157 +0,0 @@ -use super::{ - Funder, FunderField, FunderHistory, FunderOrderBy, NewFunder, NewFunderHistory, PatchFunder, -}; -use crate::graphql::utils::Direction; -use crate::model::{Crud, DbInsert, HistoryEntry}; -use crate::schema::{funder, funder_history}; -use crate::{crud_methods, db_insert}; -use diesel::{ - BoolExpressionMethods, ExpressionMethods, PgTextExpressionMethods, QueryDsl, RunQueryDsl, -}; -use thoth_errors::{ThothError, ThothResult}; -use uuid::Uuid; - -impl Crud for Funder { - type NewEntity = NewFunder; - type PatchEntity = PatchFunder; - type OrderByEntity = FunderOrderBy; - type FilterParameter1 = (); - type FilterParameter2 = (); - - fn pk(&self) -> Uuid { - self.funder_id - } - - fn all( - db: &crate::db::PgPool, - limit: i32, - offset: i32, - filter: Option, - order: Self::OrderByEntity, - _: Vec, - _: Option, - _: Option, - _: Option, - _: Option, - ) -> ThothResult> { - use crate::schema::funder::dsl::*; - let connection = db.get().unwrap(); - let mut query = funder.into_boxed(); - - match order.field { - FunderField::FunderId => match order.direction { - Direction::Asc => query = query.order(funder_id.asc()), - Direction::Desc => query = query.order(funder_id.desc()), - }, - FunderField::FunderName => match order.direction { - Direction::Asc => query = query.order(funder_name.asc()), - Direction::Desc => query = query.order(funder_name.desc()), - }, - FunderField::FunderDoi => match order.direction { - Direction::Asc => query = query.order(funder_doi.asc()), - Direction::Desc => query = query.order(funder_doi.desc()), - }, - FunderField::CreatedAt => match order.direction { - Direction::Asc => query = query.order(created_at.asc()), - Direction::Desc => query = query.order(created_at.desc()), - }, - FunderField::UpdatedAt => match order.direction { - Direction::Asc => query = query.order(updated_at.asc()), - Direction::Desc => query = query.order(updated_at.desc()), - }, - } - if let Some(filter) = filter { - query = query.filter( - funder_name - .ilike(format!("%{}%", filter)) - .or(funder_doi.ilike(format!("%{}%", filter))), - ); - } - match query - .limit(limit.into()) - .offset(offset.into()) - .load::(&connection) - { - Ok(t) => Ok(t), - Err(e) => Err(ThothError::from(e)), - } - } - - fn count( - db: &crate::db::PgPool, - filter: Option, - _: Vec, - _: Option, - _: Option, - ) -> ThothResult { - use crate::schema::funder::dsl::*; - let connection = db.get().unwrap(); - let mut query = funder.into_boxed(); - if let Some(filter) = filter { - query = query.filter( - funder_name - .ilike(format!("%{}%", filter)) - .or(funder_doi.ilike(format!("%{}%", filter))), - ); - } - - // `SELECT COUNT(*)` in postgres returns a BIGINT, which diesel parses as i64. Juniper does - // not implement i64 yet, only i32. The only sensible way, albeit shameful, to solve this - // is converting i64 to string and then parsing it as i32. This should work until we reach - // 2147483647 records - if you are fixing this bug, congratulations on book number 2147483647! - match query.count().get_result::(&connection) { - Ok(t) => Ok(t.to_string().parse::().unwrap()), - Err(e) => Err(ThothError::from(e)), - } - } - - fn publisher_id(&self, _db: &crate::db::PgPool) -> ThothResult { - Err(ThothError::InternalError( - "Method publisher_id() is not supported for Funder objects".to_string(), - )) - } - - crud_methods!(funder::table, funder::dsl::funder); -} - -impl HistoryEntry for Funder { - type NewHistoryEntity = NewFunderHistory; - - fn new_history_entry(&self, account_id: &Uuid) -> Self::NewHistoryEntity { - Self::NewHistoryEntity { - funder_id: self.funder_id, - account_id: *account_id, - data: serde_json::Value::String(serde_json::to_string(&self).unwrap()), - } - } -} - -impl DbInsert for NewFunderHistory { - type MainEntity = FunderHistory; - - db_insert!(funder_history::table); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_funder_pk() { - let funder: Funder = Default::default(); - assert_eq!(funder.pk(), funder.funder_id); - } - - #[test] - fn test_new_funder_history_from_funder() { - let funder: Funder = Default::default(); - let account_id: Uuid = Default::default(); - let new_funder_history = funder.new_history_entry(&account_id); - assert_eq!(new_funder_history.funder_id, funder.funder_id); - assert_eq!(new_funder_history.account_id, account_id); - assert_eq!( - new_funder_history.data, - serde_json::Value::String(serde_json::to_string(&funder).unwrap()) - ); - } -} diff --git a/thoth-api/src/model/funder/mod.rs b/thoth-api/src/model/funder/mod.rs deleted file mode 100644 index ca0dad44..00000000 --- a/thoth-api/src/model/funder/mod.rs +++ /dev/null @@ -1,151 +0,0 @@ -use serde::Deserialize; -use serde::Serialize; -use std::fmt; -use strum::Display; -use strum::EnumString; -use uuid::Uuid; - -use crate::graphql::utils::Direction; -use crate::model::Doi; -use crate::model::Timestamp; -#[cfg(feature = "backend")] -use crate::schema::funder; -#[cfg(feature = "backend")] -use crate::schema::funder_history; - -#[cfg_attr( - feature = "backend", - derive(juniper::GraphQLEnum), - graphql(description = "Field to use when sorting funders list") -)] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, EnumString, Display)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum FunderField { - #[strum(serialize = "ID")] - FunderId, - #[strum(serialize = "Funder")] - FunderName, - #[strum(serialize = "DOI")] - FunderDoi, - CreatedAt, - UpdatedAt, -} - -#[cfg_attr(feature = "backend", derive(Queryable))] -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct Funder { - pub funder_id: Uuid, - pub funder_name: String, - pub funder_doi: Option, - pub created_at: Timestamp, - pub updated_at: Timestamp, -} - -#[cfg_attr( - feature = "backend", - derive(juniper::GraphQLInputObject, Insertable), - table_name = "funder" -)] -pub struct NewFunder { - pub funder_name: String, - pub funder_doi: Option, -} - -#[cfg_attr( - feature = "backend", - derive(juniper::GraphQLInputObject, AsChangeset), - changeset_options(treat_none_as_null = "true"), - table_name = "funder" -)] -pub struct PatchFunder { - pub funder_id: Uuid, - pub funder_name: String, - pub funder_doi: Option, -} - -#[cfg_attr(feature = "backend", derive(Queryable))] -pub struct FunderHistory { - pub funder_history_id: Uuid, - pub funder_id: Uuid, - pub account_id: Uuid, - pub data: serde_json::Value, - pub timestamp: Timestamp, -} - -#[cfg_attr(feature = "backend", derive(Insertable), table_name = "funder_history")] -pub struct NewFunderHistory { - pub funder_id: Uuid, - pub account_id: Uuid, - pub data: serde_json::Value, -} - -#[cfg_attr( - feature = "backend", - derive(juniper::GraphQLInputObject), - graphql(description = "Field and order to use when sorting funders list") -)] -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -pub struct FunderOrderBy { - pub field: FunderField, - pub direction: Direction, -} - -impl Default for FunderField { - fn default() -> Self { - FunderField::FunderName - } -} - -impl fmt::Display for Funder { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if let Some(doi) = &self.funder_doi { - write!(f, "{} - {}", &self.funder_name, doi) - } else { - write!(f, "{}", &self.funder_name) - } - } -} - -#[test] -fn test_funderfield_default() { - let fundfield: FunderField = Default::default(); - assert_eq!(fundfield, FunderField::FunderName); -} - -#[test] -fn test_funderfield_display() { - assert_eq!(format!("{}", FunderField::FunderId), "ID"); - assert_eq!(format!("{}", FunderField::FunderName), "Funder"); - assert_eq!(format!("{}", FunderField::FunderDoi), "DOI"); - assert_eq!(format!("{}", FunderField::CreatedAt), "CreatedAt"); - assert_eq!(format!("{}", FunderField::UpdatedAt), "UpdatedAt"); -} - -#[test] -fn test_funderfield_fromstr() { - use std::str::FromStr; - assert_eq!(FunderField::from_str("ID").unwrap(), FunderField::FunderId); - assert_eq!( - FunderField::from_str("Funder").unwrap(), - FunderField::FunderName - ); - assert_eq!( - FunderField::from_str("DOI").unwrap(), - FunderField::FunderDoi - ); - assert_eq!( - FunderField::from_str("CreatedAt").unwrap(), - FunderField::CreatedAt - ); - assert_eq!( - FunderField::from_str("UpdatedAt").unwrap(), - FunderField::UpdatedAt - ); - assert!(FunderField::from_str("FunderID").is_err()); - assert!(FunderField::from_str("Website").is_err()); - assert!(FunderField::from_str("Fundings").is_err()); -} - -#[cfg(feature = "backend")] -pub mod crud; diff --git a/thoth-api/src/model/funding/crud.rs b/thoth-api/src/model/funding/crud.rs index f4a9d4e2..144a35d7 100644 --- a/thoth-api/src/model/funding/crud.rs +++ b/thoth-api/src/model/funding/crud.rs @@ -38,7 +38,7 @@ impl Crud for Funding { .select(( funding_id, work_id, - funder_id, + institution_id, program, project_name, project_shortname, @@ -58,9 +58,9 @@ impl Crud for Funding { Direction::Asc => query = query.order(work_id.asc()), Direction::Desc => query = query.order(work_id.desc()), }, - FundingField::FunderId => match order.direction { - Direction::Asc => query = query.order(funder_id.asc()), - Direction::Desc => query = query.order(funder_id.desc()), + FundingField::InstitutionId => match order.direction { + Direction::Asc => query = query.order(institution_id.asc()), + Direction::Desc => query = query.order(institution_id.desc()), }, FundingField::Program => match order.direction { Direction::Asc => query = query.order(program.asc()), @@ -101,7 +101,7 @@ impl Crud for Funding { query = query.filter(work_id.eq(pid)); } if let Some(pid) = parent_id_2 { - query = query.filter(funder_id.eq(pid)); + query = query.filter(institution_id.eq(pid)); } match query .limit(limit.into()) diff --git a/thoth-api/src/model/funding/mod.rs b/thoth-api/src/model/funding/mod.rs index dab2f5ef..cf0e8409 100644 --- a/thoth-api/src/model/funding/mod.rs +++ b/thoth-api/src/model/funding/mod.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::model::funder::Funder; +use crate::model::institution::Institution; use crate::model::work::WorkWithRelations; use crate::model::Timestamp; #[cfg(feature = "backend")] @@ -15,7 +15,7 @@ use crate::schema::funding_history; graphql(description = "Field to use when sorting fundings list") )] pub enum FundingField { - FunderId, + InstitutionId, WorkId, FundingId, Program, @@ -33,7 +33,7 @@ pub enum FundingField { pub struct Funding { pub funding_id: Uuid, pub work_id: Uuid, - pub funder_id: Uuid, + pub institution_id: Uuid, pub program: Option, pub project_name: Option, pub project_shortname: Option, @@ -45,16 +45,16 @@ pub struct Funding { #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] -pub struct FundingWithFunder { +pub struct FundingWithInstitution { pub funding_id: Uuid, pub work_id: Uuid, - pub funder_id: Uuid, + pub institution_id: Uuid, pub program: Option, pub project_name: Option, pub project_shortname: Option, pub grant_number: Option, pub jurisdiction: Option, - pub funder: Funder, + pub institution: Institution, } #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] @@ -70,7 +70,7 @@ pub struct FundingWithWork { )] pub struct NewFunding { pub work_id: Uuid, - pub funder_id: Uuid, + pub institution_id: Uuid, pub program: Option, pub project_name: Option, pub project_shortname: Option, @@ -87,7 +87,7 @@ pub struct NewFunding { pub struct PatchFunding { pub funding_id: Uuid, pub work_id: Uuid, - pub funder_id: Uuid, + pub institution_id: Uuid, pub program: Option, pub project_name: Option, pub project_shortname: Option, diff --git a/thoth-api/src/model/institution/crud.rs b/thoth-api/src/model/institution/crud.rs new file mode 100644 index 00000000..b1c2a134 --- /dev/null +++ b/thoth-api/src/model/institution/crud.rs @@ -0,0 +1,171 @@ +use super::{ + Institution, InstitutionField, InstitutionHistory, InstitutionOrderBy, NewInstitution, + NewInstitutionHistory, PatchInstitution, +}; +use crate::graphql::utils::Direction; +use crate::model::{Crud, DbInsert, HistoryEntry}; +use crate::schema::{institution, institution_history}; +use crate::{crud_methods, db_insert}; +use diesel::{ + BoolExpressionMethods, ExpressionMethods, PgTextExpressionMethods, QueryDsl, RunQueryDsl, +}; +use thoth_errors::{ThothError, ThothResult}; +use uuid::Uuid; + +impl Crud for Institution { + type NewEntity = NewInstitution; + type PatchEntity = PatchInstitution; + type OrderByEntity = InstitutionOrderBy; + type FilterParameter1 = (); + type FilterParameter2 = (); + + fn pk(&self) -> Uuid { + self.institution_id + } + + fn all( + db: &crate::db::PgPool, + limit: i32, + offset: i32, + filter: Option, + order: Self::OrderByEntity, + _: Vec, + _: Option, + _: Option, + _: Option, + _: Option, + ) -> ThothResult> { + use crate::schema::institution::dsl::*; + let connection = db.get().unwrap(); + let mut query = institution.into_boxed(); + + match order.field { + InstitutionField::InstitutionId => match order.direction { + Direction::Asc => query = query.order(institution_id.asc()), + Direction::Desc => query = query.order(institution_id.desc()), + }, + InstitutionField::InstitutionName => match order.direction { + Direction::Asc => query = query.order(institution_name.asc()), + Direction::Desc => query = query.order(institution_name.desc()), + }, + InstitutionField::InstitutionDoi => match order.direction { + Direction::Asc => query = query.order(institution_doi.asc()), + Direction::Desc => query = query.order(institution_doi.desc()), + }, + InstitutionField::Ror => match order.direction { + Direction::Asc => query = query.order(ror.asc()), + Direction::Desc => query = query.order(ror.desc()), + }, + InstitutionField::CountryCode => match order.direction { + Direction::Asc => query = query.order(country_code.asc()), + Direction::Desc => query = query.order(country_code.desc()), + }, + InstitutionField::CreatedAt => match order.direction { + Direction::Asc => query = query.order(created_at.asc()), + Direction::Desc => query = query.order(created_at.desc()), + }, + InstitutionField::UpdatedAt => match order.direction { + Direction::Asc => query = query.order(updated_at.asc()), + Direction::Desc => query = query.order(updated_at.desc()), + }, + } + if let Some(filter) = filter { + query = query.filter( + institution_name + .ilike(format!("%{}%", filter)) + .or(ror.ilike(format!("%{}%", filter))) + .or(institution_doi.ilike(format!("%{}%", filter))), + ); + } + match query + .limit(limit.into()) + .offset(offset.into()) + .load::(&connection) + { + Ok(t) => Ok(t), + Err(e) => Err(ThothError::from(e)), + } + } + + fn count( + db: &crate::db::PgPool, + filter: Option, + _: Vec, + _: Option, + _: Option, + ) -> ThothResult { + use crate::schema::institution::dsl::*; + let connection = db.get().unwrap(); + let mut query = institution.into_boxed(); + if let Some(filter) = filter { + query = query.filter( + institution_name + .ilike(format!("%{}%", filter)) + .or(ror.ilike(format!("%{}%", filter))) + .or(institution_doi.ilike(format!("%{}%", filter))), + ); + } + + // `SELECT COUNT(*)` in postgres returns a BIGINT, which diesel parses as i64. Juniper does + // not implement i64 yet, only i32. The only sensible way, albeit shameful, to solve this + // is converting i64 to string and then parsing it as i32. This should work until we reach + // 2147483647 records - if you are fixing this bug, congratulations on book number 2147483647! + match query.count().get_result::(&connection) { + Ok(t) => Ok(t.to_string().parse::().unwrap()), + Err(e) => Err(ThothError::from(e)), + } + } + + fn publisher_id(&self, _db: &crate::db::PgPool) -> ThothResult { + Err(ThothError::InternalError( + "Method publisher_id() is not supported for Institution objects".to_string(), + )) + } + + crud_methods!(institution::table, institution::dsl::institution); +} + +impl HistoryEntry for Institution { + type NewHistoryEntity = NewInstitutionHistory; + + fn new_history_entry(&self, account_id: &Uuid) -> Self::NewHistoryEntity { + Self::NewHistoryEntity { + institution_id: self.institution_id, + account_id: *account_id, + data: serde_json::Value::String(serde_json::to_string(&self).unwrap()), + } + } +} + +impl DbInsert for NewInstitutionHistory { + type MainEntity = InstitutionHistory; + + db_insert!(institution_history::table); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_institution_pk() { + let institution: Institution = Default::default(); + assert_eq!(institution.pk(), institution.institution_id); + } + + #[test] + fn test_new_institution_history_from_institution() { + let institution: Institution = Default::default(); + let account_id: Uuid = Default::default(); + let new_institution_history = institution.new_history_entry(&account_id); + assert_eq!( + new_institution_history.institution_id, + institution.institution_id + ); + assert_eq!(new_institution_history.account_id, account_id); + assert_eq!( + new_institution_history.data, + serde_json::Value::String(serde_json::to_string(&institution).unwrap()) + ); + } +} diff --git a/thoth-api/src/model/institution/mod.rs b/thoth-api/src/model/institution/mod.rs new file mode 100644 index 00000000..d23fd7b3 --- /dev/null +++ b/thoth-api/src/model/institution/mod.rs @@ -0,0 +1,1541 @@ +use serde::Deserialize; +use serde::Serialize; +use std::fmt; +use strum::Display; +use strum::EnumString; +use uuid::Uuid; + +use crate::graphql::utils::Direction; +use crate::model::Doi; +use crate::model::Ror; +use crate::model::Timestamp; +#[cfg(feature = "backend")] +use crate::schema::institution; +#[cfg(feature = "backend")] +use crate::schema::institution_history; + +#[cfg_attr( + feature = "backend", + derive(juniper::GraphQLEnum), + graphql(description = "Field to use when sorting institutions list") +)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, EnumString, Display)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum InstitutionField { + #[strum(serialize = "ID")] + InstitutionId, + #[strum(serialize = "Institution")] + InstitutionName, + #[strum(serialize = "DOI")] + InstitutionDoi, + #[strum(serialize = "ROR ID")] + Ror, + #[strum(serialize = "Country")] + CountryCode, + CreatedAt, + UpdatedAt, +} + +#[cfg_attr(feature = "backend", derive(Queryable))] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Institution { + pub institution_id: Uuid, + pub institution_name: String, + pub institution_doi: Option, + pub created_at: Timestamp, + pub updated_at: Timestamp, + pub ror: Option, + pub country_code: Option, +} + +#[cfg_attr( + feature = "backend", + derive(juniper::GraphQLInputObject, Insertable), + table_name = "institution" +)] +pub struct NewInstitution { + pub institution_name: String, + pub institution_doi: Option, + pub ror: Option, + pub country_code: Option, +} + +#[cfg_attr( + feature = "backend", + derive(juniper::GraphQLInputObject, AsChangeset), + changeset_options(treat_none_as_null = "true"), + table_name = "institution" +)] +pub struct PatchInstitution { + pub institution_id: Uuid, + pub institution_name: String, + pub institution_doi: Option, + pub ror: Option, + pub country_code: Option, +} + +#[cfg_attr(feature = "backend", derive(DbEnum, juniper::GraphQLEnum))] +#[cfg_attr(feature = "backend", DieselType = "Country_code")] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, EnumString, Display)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CountryCode { + #[strum(serialize = "Afghanistan")] + Afg, + #[strum(serialize = "Åland Islands")] + Ala, + #[strum(serialize = "Albania")] + Alb, + #[strum(serialize = "Algeria")] + Dza, + #[strum(serialize = "American Samoa")] + Asm, + #[strum(serialize = "Andorra")] + And, + #[strum(serialize = "Angola")] + Ago, + #[strum(serialize = "Anguilla")] + Aia, + #[strum(serialize = "Antarctica")] + Ata, + #[strum(serialize = "Antigua and Barbuda")] + Atg, + #[strum(serialize = "Argentina")] + Arg, + #[strum(serialize = "Armenia")] + Arm, + #[strum(serialize = "Aruba")] + Abw, + #[strum(serialize = "Australia")] + Aus, + #[strum(serialize = "Austria")] + Aut, + #[strum(serialize = "Azerbaijan")] + Aze, + #[strum(serialize = "Bahamas")] + Bhs, + #[strum(serialize = "Bahrain")] + Bhr, + #[strum(serialize = "Bangladesh")] + Bgd, + #[strum(serialize = "Barbados")] + Brb, + #[strum(serialize = "Belarus")] + Blr, + #[strum(serialize = "Belgium")] + Bel, + #[strum(serialize = "Belize")] + Blz, + #[strum(serialize = "Benin")] + Ben, + #[strum(serialize = "Bermuda")] + Bmu, + #[strum(serialize = "Bhutan")] + Btn, + #[strum(serialize = "Bolivia")] + Bol, + #[strum(serialize = "Bonaire, Sint Eustatius and Saba")] + Bes, + #[strum(serialize = "Bosnia and Herzegovina")] + Bih, + #[strum(serialize = "Botswana")] + Bwa, + #[strum(serialize = "Bouvet Island")] + Bvt, + #[strum(serialize = "Brazil")] + Bra, + #[strum(serialize = "British Indian Ocean Territory")] + Iot, + #[strum(serialize = "Brunei")] + Brn, + #[strum(serialize = "Bulgaria")] + Bgr, + #[strum(serialize = "Burkina Faso")] + Bfa, + #[strum(serialize = "Burundi")] + Bdi, + #[strum(serialize = "Cabo Verde")] + Cpv, + #[strum(serialize = "Cambodia")] + Khm, + #[strum(serialize = "Cameroon")] + Cmr, + #[strum(serialize = "Canada")] + Can, + #[strum(serialize = "Cayman Islands")] + Cym, + #[strum(serialize = "Central African Republic")] + Caf, + #[strum(serialize = "Chad")] + Tcd, + #[strum(serialize = "Chile")] + Chl, + #[strum(serialize = "China")] + Chn, + #[strum(serialize = "Christmas Island")] + Cxr, + #[strum(serialize = "Cocos (Keeling) Islands")] + Cck, + #[strum(serialize = "Colombia")] + Col, + #[strum(serialize = "Comoros")] + Com, + #[strum(serialize = "Cook Islands")] + Cok, + #[strum(serialize = "Costa Rica")] + Cri, + #[strum(serialize = "Côte d'Ivoire")] + Civ, + #[strum(serialize = "Croatia")] + Hrv, + #[strum(serialize = "Cuba")] + Cub, + #[strum(serialize = "Curaçao")] + Cuw, + #[strum(serialize = "Cyprus")] + Cyp, + #[strum(serialize = "Czechia")] + Cze, + #[strum(serialize = "Democratic Republic of the Congo")] + Cod, + #[strum(serialize = "Denmark")] + Dnk, + #[strum(serialize = "Djibouti")] + Dji, + #[strum(serialize = "Dominica")] + Dma, + #[strum(serialize = "Dominican Republic")] + Dom, + #[strum(serialize = "Ecuador")] + Ecu, + #[strum(serialize = "Egypt")] + Egy, + #[strum(serialize = "El Salvador")] + Slv, + #[strum(serialize = "Equatorial Guinea")] + Gnq, + #[strum(serialize = "Eritrea")] + Eri, + #[strum(serialize = "Estonia")] + Est, + #[strum(serialize = "Eswatini")] + Swz, + #[strum(serialize = "Ethiopia")] + Eth, + #[strum(serialize = "Falkland Islands")] + Flk, + #[strum(serialize = "Faroe Islands")] + Fro, + #[strum(serialize = "Fiji")] + Fji, + #[strum(serialize = "Finland")] + Fin, + #[strum(serialize = "France")] + Fra, + #[strum(serialize = "French Guiana")] + Guf, + #[strum(serialize = "French Polynesia")] + Pyf, + #[strum(serialize = "French Southern Territories")] + Atf, + #[strum(serialize = "Gabon")] + Gab, + #[strum(serialize = "Gambia")] + Gmb, + #[strum(serialize = "Georgia")] + Geo, + #[strum(serialize = "Germany")] + Deu, + #[strum(serialize = "Ghana")] + Gha, + #[strum(serialize = "Gibraltar")] + Gib, + #[strum(serialize = "Greece")] + Grc, + #[strum(serialize = "Greenland")] + Grl, + #[strum(serialize = "Grenada")] + Grd, + #[strum(serialize = "Guadeloupe")] + Glp, + #[strum(serialize = "Guam")] + Gum, + #[strum(serialize = "Guatemala")] + Gtm, + #[strum(serialize = "Guernsey")] + Ggy, + #[strum(serialize = "Guinea")] + Gin, + #[strum(serialize = "Guinea-Bissau")] + Gnb, + #[strum(serialize = "Guyana")] + Guy, + #[strum(serialize = "Haiti")] + Hti, + #[strum(serialize = "Heard Island and McDonald Islands")] + Hmd, + #[strum(serialize = "Honduras")] + Hnd, + #[strum(serialize = "Hong Kong")] + Hkg, + #[strum(serialize = "Hungary")] + Hun, + #[strum(serialize = "Iceland")] + Isl, + #[strum(serialize = "India")] + Ind, + #[strum(serialize = "Indonesia")] + Idn, + #[strum(serialize = "Iran")] + Irn, + #[strum(serialize = "Iraq")] + Irq, + #[strum(serialize = "Ireland")] + Irl, + #[strum(serialize = "Isle of Man")] + Imn, + #[strum(serialize = "Israel")] + Isr, + #[strum(serialize = "Italy")] + Ita, + #[strum(serialize = "Jamaica")] + Jam, + #[strum(serialize = "Japan")] + Jpn, + #[strum(serialize = "Jersey")] + Jey, + #[strum(serialize = "Jordan")] + Jor, + #[strum(serialize = "Kazakhstan")] + Kaz, + #[strum(serialize = "Kenya")] + Ken, + #[strum(serialize = "Kiribati")] + Kir, + #[strum(serialize = "Kuwait")] + Kwt, + #[strum(serialize = "Kyrgyzstan")] + Kgz, + #[strum(serialize = "Laos")] + Lao, + #[strum(serialize = "Latvia")] + Lva, + #[strum(serialize = "Lebanon")] + Lbn, + #[strum(serialize = "Lesotho")] + Lso, + #[strum(serialize = "Liberia")] + Lbr, + #[strum(serialize = "Libya")] + Lby, + #[strum(serialize = "Liechtenstein")] + Lie, + #[strum(serialize = "Lithuania")] + Ltu, + #[strum(serialize = "Luxembourg")] + Lux, + #[strum(serialize = "Macao")] + Mac, + #[strum(serialize = "Madagascar")] + Mdg, + #[strum(serialize = "Malawi")] + Mwi, + #[strum(serialize = "Malaysia")] + Mys, + #[strum(serialize = "Maldives")] + Mdv, + #[strum(serialize = "Mali")] + Mli, + #[strum(serialize = "Malta")] + Mlt, + #[strum(serialize = "Marshall Islands")] + Mhl, + #[strum(serialize = "Martinique")] + Mtq, + #[strum(serialize = "Mauritania")] + Mrt, + #[strum(serialize = "Mauritius")] + Mus, + #[strum(serialize = "Mayotte")] + Myt, + #[strum(serialize = "Mexico")] + Mex, + #[strum(serialize = "Micronesia")] + Fsm, + #[strum(serialize = "Moldova")] + Mda, + #[strum(serialize = "Monaco")] + Mco, + #[strum(serialize = "Mongolia")] + Mng, + #[strum(serialize = "Montenegro")] + Mne, + #[strum(serialize = "Montserrat")] + Msr, + #[strum(serialize = "Morocco")] + Mar, + #[strum(serialize = "Mozambique")] + Moz, + #[strum(serialize = "Myanmar")] + Mmr, + #[strum(serialize = "Namibia")] + Nam, + #[strum(serialize = "Nauru")] + Nru, + #[strum(serialize = "Nepal")] + Npl, + #[strum(serialize = "Netherlands")] + Nld, + #[strum(serialize = "New Caledonia")] + Ncl, + #[strum(serialize = "New Zealand")] + Nzl, + #[strum(serialize = "Nicaragua")] + Nic, + #[strum(serialize = "Niger")] + Ner, + #[strum(serialize = "Nigeria")] + Nga, + #[strum(serialize = "Niue")] + Niu, + #[strum(serialize = "Norfolk Island")] + Nfk, + #[strum(serialize = "North Korea")] + Prk, + #[strum(serialize = "North Macedonia")] + Mkd, + #[strum(serialize = "Northern Mariana Islands")] + Mnp, + #[strum(serialize = "Norway")] + Nor, + #[strum(serialize = "Oman")] + Omn, + #[strum(serialize = "Pakistan")] + Pak, + #[strum(serialize = "Palau")] + Plw, + #[strum(serialize = "Palestine")] + Pse, + #[strum(serialize = "Panama")] + Pan, + #[strum(serialize = "Papua New Guinea")] + Png, + #[strum(serialize = "Paraguay")] + Pry, + #[strum(serialize = "Peru")] + Per, + #[strum(serialize = "Philippines")] + Phl, + #[strum(serialize = "Pitcairn")] + Pcn, + #[strum(serialize = "Poland")] + Pol, + #[strum(serialize = "Portugal")] + Prt, + #[strum(serialize = "Puerto Rico")] + Pri, + #[strum(serialize = "Qatar")] + Qat, + #[strum(serialize = "Republic of the Congo")] + Cog, + #[strum(serialize = "Réunion")] + Reu, + #[strum(serialize = "Romania")] + Rou, + #[strum(serialize = "Russia")] + Rus, + #[strum(serialize = "Rwanda")] + Rwa, + #[strum(serialize = "Saint Barthélemy")] + Blm, + #[strum(serialize = "Saint Helena, Ascension and Tristan da Cunha")] + Shn, + #[strum(serialize = "Saint Kitts and Nevis")] + Kna, + #[strum(serialize = "Saint Lucia")] + Lca, + #[strum(serialize = "Saint Martin")] + Maf, + #[strum(serialize = "Saint Pierre and Miquelon")] + Spm, + #[strum(serialize = "Saint Vincent and the Grenadines")] + Vct, + #[strum(serialize = "Samoa")] + Wsm, + #[strum(serialize = "San Marino")] + Smr, + #[strum(serialize = "Sao Tome and Principe")] + Stp, + #[strum(serialize = "Saudi Arabia")] + Sau, + #[strum(serialize = "Senegal")] + Sen, + #[strum(serialize = "Serbia")] + Srb, + #[strum(serialize = "Seychelles")] + Syc, + #[strum(serialize = "Sierra Leone")] + Sle, + #[strum(serialize = "Singapore")] + Sgp, + #[strum(serialize = "Sint Maarten")] + Sxm, + #[strum(serialize = "Slovakia")] + Svk, + #[strum(serialize = "Slovenia")] + Svn, + #[strum(serialize = "Solomon Islands")] + Slb, + #[strum(serialize = "Somalia")] + Som, + #[strum(serialize = "South Africa")] + Zaf, + #[strum(serialize = "South Georgia and the South Sandwich Islands")] + Sgs, + #[strum(serialize = "South Korea")] + Kor, + #[strum(serialize = "South Sudan")] + Ssd, + #[strum(serialize = "Spain")] + Esp, + #[strum(serialize = "Sri Lanka")] + Lka, + #[strum(serialize = "Sudan")] + Sdn, + #[strum(serialize = "Suriname")] + Sur, + #[strum(serialize = "Svalbard and Jan Mayen")] + Sjm, + #[strum(serialize = "Sweden")] + Swe, + #[strum(serialize = "Switzerland")] + Che, + #[strum(serialize = "Syria")] + Syr, + #[strum(serialize = "Taiwan")] + Twn, + #[strum(serialize = "Tajikistan")] + Tjk, + #[strum(serialize = "Tanzania")] + Tza, + #[strum(serialize = "Thailand")] + Tha, + #[strum(serialize = "Timor-Leste")] + Tls, + #[strum(serialize = "Togo")] + Tgo, + #[strum(serialize = "Tokelau")] + Tkl, + #[strum(serialize = "Tonga")] + Ton, + #[strum(serialize = "Trinidad and Tobago")] + Tto, + #[strum(serialize = "Tunisia")] + Tun, + #[strum(serialize = "Turkey")] + Tur, + #[strum(serialize = "Turkmenistan")] + Tkm, + #[strum(serialize = "Turks and Caicos Islands")] + Tca, + #[strum(serialize = "Tuvalu")] + Tuv, + #[strum(serialize = "Uganda")] + Uga, + #[strum(serialize = "Ukraine")] + Ukr, + #[strum(serialize = "United Arab Emirates")] + Are, + #[strum(serialize = "United Kingdom")] + Gbr, + #[strum(serialize = "United States Minor Outlying Islands")] + Umi, + #[strum(serialize = "United States of America")] + Usa, + #[strum(serialize = "Uruguay")] + Ury, + #[strum(serialize = "Uzbekistan")] + Uzb, + #[strum(serialize = "Vanuatu")] + Vut, + #[strum(serialize = "Vatican City")] + Vat, + #[strum(serialize = "Venezuela")] + Ven, + #[strum(serialize = "Viet Nam")] + Vnm, + #[strum(serialize = "Virgin Islands (British)")] + Vgb, + #[strum(serialize = "Virgin Islands (U.S.)")] + Vir, + #[strum(serialize = "Wallis and Futuna")] + Wlf, + #[strum(serialize = "Western Sahara")] + Esh, + #[strum(serialize = "Yemen")] + Yem, + #[strum(serialize = "Zambia")] + Zmb, + #[strum(serialize = "Zimbabwe")] + Zwe, +} + +#[cfg_attr(feature = "backend", derive(Queryable))] +pub struct InstitutionHistory { + pub institution_history_id: Uuid, + pub institution_id: Uuid, + pub account_id: Uuid, + pub data: serde_json::Value, + pub timestamp: Timestamp, +} + +#[cfg_attr( + feature = "backend", + derive(Insertable), + table_name = "institution_history" +)] +pub struct NewInstitutionHistory { + pub institution_id: Uuid, + pub account_id: Uuid, + pub data: serde_json::Value, +} + +#[cfg_attr( + feature = "backend", + derive(juniper::GraphQLInputObject), + graphql(description = "Field and order to use when sorting institutions list") +)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub struct InstitutionOrderBy { + pub field: InstitutionField, + pub direction: Direction, +} + +impl Default for InstitutionField { + fn default() -> Self { + InstitutionField::InstitutionName + } +} + +impl fmt::Display for Institution { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(ror) = &self.ror { + write!(f, "{} - {}", &self.institution_name, ror) + } else if let Some(doi) = &self.institution_doi { + write!(f, "{} - {}", &self.institution_name, doi) + } else { + write!(f, "{}", &self.institution_name) + } + } +} + +#[test] +fn test_institutionfield_default() { + let fundfield: InstitutionField = Default::default(); + assert_eq!(fundfield, InstitutionField::InstitutionName); +} + +#[test] +fn test_institutionfield_display() { + assert_eq!(format!("{}", InstitutionField::InstitutionId), "ID"); + assert_eq!( + format!("{}", InstitutionField::InstitutionName), + "Institution" + ); + assert_eq!(format!("{}", InstitutionField::InstitutionDoi), "DOI"); + assert_eq!(format!("{}", InstitutionField::Ror), "ROR ID"); + assert_eq!(format!("{}", InstitutionField::CountryCode), "Country"); + assert_eq!(format!("{}", InstitutionField::CreatedAt), "CreatedAt"); + assert_eq!(format!("{}", InstitutionField::UpdatedAt), "UpdatedAt"); +} + +#[test] +fn test_institutionfield_fromstr() { + use std::str::FromStr; + assert_eq!( + InstitutionField::from_str("ID").unwrap(), + InstitutionField::InstitutionId + ); + assert_eq!( + InstitutionField::from_str("Institution").unwrap(), + InstitutionField::InstitutionName + ); + assert_eq!( + InstitutionField::from_str("DOI").unwrap(), + InstitutionField::InstitutionDoi + ); + assert_eq!( + InstitutionField::from_str("ROR ID").unwrap(), + InstitutionField::Ror + ); + assert_eq!( + InstitutionField::from_str("Country").unwrap(), + InstitutionField::CountryCode + ); + assert_eq!( + InstitutionField::from_str("CreatedAt").unwrap(), + InstitutionField::CreatedAt + ); + assert_eq!( + InstitutionField::from_str("UpdatedAt").unwrap(), + InstitutionField::UpdatedAt + ); + assert!(InstitutionField::from_str("InstitutionID").is_err()); + assert!(InstitutionField::from_str("Website").is_err()); + assert!(InstitutionField::from_str("Fundings").is_err()); +} + +#[test] +fn test_countrycode_display() { + assert_eq!(format!("{}", CountryCode::Afg), "Afghanistan"); + assert_eq!(format!("{}", CountryCode::Ala), "Åland Islands"); + assert_eq!(format!("{}", CountryCode::Alb), "Albania"); + assert_eq!(format!("{}", CountryCode::Dza), "Algeria"); + assert_eq!(format!("{}", CountryCode::Asm), "American Samoa"); + assert_eq!(format!("{}", CountryCode::And), "Andorra"); + assert_eq!(format!("{}", CountryCode::Ago), "Angola"); + assert_eq!(format!("{}", CountryCode::Aia), "Anguilla"); + assert_eq!(format!("{}", CountryCode::Ata), "Antarctica"); + assert_eq!(format!("{}", CountryCode::Atg), "Antigua and Barbuda"); + assert_eq!(format!("{}", CountryCode::Arg), "Argentina"); + assert_eq!(format!("{}", CountryCode::Arm), "Armenia"); + assert_eq!(format!("{}", CountryCode::Abw), "Aruba"); + assert_eq!(format!("{}", CountryCode::Aus), "Australia"); + assert_eq!(format!("{}", CountryCode::Aut), "Austria"); + assert_eq!(format!("{}", CountryCode::Aze), "Azerbaijan"); + assert_eq!(format!("{}", CountryCode::Bhs), "Bahamas"); + assert_eq!(format!("{}", CountryCode::Bhr), "Bahrain"); + assert_eq!(format!("{}", CountryCode::Bgd), "Bangladesh"); + assert_eq!(format!("{}", CountryCode::Brb), "Barbados"); + assert_eq!(format!("{}", CountryCode::Blr), "Belarus"); + assert_eq!(format!("{}", CountryCode::Bel), "Belgium"); + assert_eq!(format!("{}", CountryCode::Blz), "Belize"); + assert_eq!(format!("{}", CountryCode::Ben), "Benin"); + assert_eq!(format!("{}", CountryCode::Bmu), "Bermuda"); + assert_eq!(format!("{}", CountryCode::Btn), "Bhutan"); + assert_eq!(format!("{}", CountryCode::Bol), "Bolivia"); + assert_eq!( + format!("{}", CountryCode::Bes), + "Bonaire, Sint Eustatius and Saba" + ); + assert_eq!(format!("{}", CountryCode::Bih), "Bosnia and Herzegovina"); + assert_eq!(format!("{}", CountryCode::Bwa), "Botswana"); + assert_eq!(format!("{}", CountryCode::Bvt), "Bouvet Island"); + assert_eq!(format!("{}", CountryCode::Bra), "Brazil"); + assert_eq!( + format!("{}", CountryCode::Iot), + "British Indian Ocean Territory" + ); + assert_eq!(format!("{}", CountryCode::Brn), "Brunei"); + assert_eq!(format!("{}", CountryCode::Bgr), "Bulgaria"); + assert_eq!(format!("{}", CountryCode::Bfa), "Burkina Faso"); + assert_eq!(format!("{}", CountryCode::Bdi), "Burundi"); + assert_eq!(format!("{}", CountryCode::Cpv), "Cabo Verde"); + assert_eq!(format!("{}", CountryCode::Khm), "Cambodia"); + assert_eq!(format!("{}", CountryCode::Cmr), "Cameroon"); + assert_eq!(format!("{}", CountryCode::Can), "Canada"); + assert_eq!(format!("{}", CountryCode::Cym), "Cayman Islands"); + assert_eq!(format!("{}", CountryCode::Caf), "Central African Republic"); + assert_eq!(format!("{}", CountryCode::Tcd), "Chad"); + assert_eq!(format!("{}", CountryCode::Chl), "Chile"); + assert_eq!(format!("{}", CountryCode::Chn), "China"); + assert_eq!(format!("{}", CountryCode::Cxr), "Christmas Island"); + assert_eq!(format!("{}", CountryCode::Cck), "Cocos (Keeling) Islands"); + assert_eq!(format!("{}", CountryCode::Col), "Colombia"); + assert_eq!(format!("{}", CountryCode::Com), "Comoros"); + assert_eq!(format!("{}", CountryCode::Cok), "Cook Islands"); + assert_eq!(format!("{}", CountryCode::Cri), "Costa Rica"); + assert_eq!(format!("{}", CountryCode::Civ), "Côte d'Ivoire"); + assert_eq!(format!("{}", CountryCode::Hrv), "Croatia"); + assert_eq!(format!("{}", CountryCode::Cub), "Cuba"); + assert_eq!(format!("{}", CountryCode::Cuw), "Curaçao"); + assert_eq!(format!("{}", CountryCode::Cyp), "Cyprus"); + assert_eq!(format!("{}", CountryCode::Cze), "Czechia"); + assert_eq!( + format!("{}", CountryCode::Cod), + "Democratic Republic of the Congo" + ); + assert_eq!(format!("{}", CountryCode::Dnk), "Denmark"); + assert_eq!(format!("{}", CountryCode::Dji), "Djibouti"); + assert_eq!(format!("{}", CountryCode::Dma), "Dominica"); + assert_eq!(format!("{}", CountryCode::Dom), "Dominican Republic"); + assert_eq!(format!("{}", CountryCode::Ecu), "Ecuador"); + assert_eq!(format!("{}", CountryCode::Egy), "Egypt"); + assert_eq!(format!("{}", CountryCode::Slv), "El Salvador"); + assert_eq!(format!("{}", CountryCode::Gnq), "Equatorial Guinea"); + assert_eq!(format!("{}", CountryCode::Eri), "Eritrea"); + assert_eq!(format!("{}", CountryCode::Est), "Estonia"); + assert_eq!(format!("{}", CountryCode::Swz), "Eswatini"); + assert_eq!(format!("{}", CountryCode::Eth), "Ethiopia"); + assert_eq!(format!("{}", CountryCode::Flk), "Falkland Islands"); + assert_eq!(format!("{}", CountryCode::Fro), "Faroe Islands"); + assert_eq!(format!("{}", CountryCode::Fji), "Fiji"); + assert_eq!(format!("{}", CountryCode::Fin), "Finland"); + assert_eq!(format!("{}", CountryCode::Fra), "France"); + assert_eq!(format!("{}", CountryCode::Guf), "French Guiana"); + assert_eq!(format!("{}", CountryCode::Pyf), "French Polynesia"); + assert_eq!( + format!("{}", CountryCode::Atf), + "French Southern Territories" + ); + assert_eq!(format!("{}", CountryCode::Gab), "Gabon"); + assert_eq!(format!("{}", CountryCode::Gmb), "Gambia"); + assert_eq!(format!("{}", CountryCode::Geo), "Georgia"); + assert_eq!(format!("{}", CountryCode::Deu), "Germany"); + assert_eq!(format!("{}", CountryCode::Gha), "Ghana"); + assert_eq!(format!("{}", CountryCode::Gib), "Gibraltar"); + assert_eq!(format!("{}", CountryCode::Grc), "Greece"); + assert_eq!(format!("{}", CountryCode::Grl), "Greenland"); + assert_eq!(format!("{}", CountryCode::Grd), "Grenada"); + assert_eq!(format!("{}", CountryCode::Glp), "Guadeloupe"); + assert_eq!(format!("{}", CountryCode::Gum), "Guam"); + assert_eq!(format!("{}", CountryCode::Gtm), "Guatemala"); + assert_eq!(format!("{}", CountryCode::Ggy), "Guernsey"); + assert_eq!(format!("{}", CountryCode::Gin), "Guinea"); + assert_eq!(format!("{}", CountryCode::Gnb), "Guinea-Bissau"); + assert_eq!(format!("{}", CountryCode::Guy), "Guyana"); + assert_eq!(format!("{}", CountryCode::Hti), "Haiti"); + assert_eq!( + format!("{}", CountryCode::Hmd), + "Heard Island and McDonald Islands" + ); + assert_eq!(format!("{}", CountryCode::Hnd), "Honduras"); + assert_eq!(format!("{}", CountryCode::Hkg), "Hong Kong"); + assert_eq!(format!("{}", CountryCode::Hun), "Hungary"); + assert_eq!(format!("{}", CountryCode::Isl), "Iceland"); + assert_eq!(format!("{}", CountryCode::Ind), "India"); + assert_eq!(format!("{}", CountryCode::Idn), "Indonesia"); + assert_eq!(format!("{}", CountryCode::Irn), "Iran"); + assert_eq!(format!("{}", CountryCode::Irq), "Iraq"); + assert_eq!(format!("{}", CountryCode::Irl), "Ireland"); + assert_eq!(format!("{}", CountryCode::Imn), "Isle of Man"); + assert_eq!(format!("{}", CountryCode::Isr), "Israel"); + assert_eq!(format!("{}", CountryCode::Ita), "Italy"); + assert_eq!(format!("{}", CountryCode::Jam), "Jamaica"); + assert_eq!(format!("{}", CountryCode::Jpn), "Japan"); + assert_eq!(format!("{}", CountryCode::Jey), "Jersey"); + assert_eq!(format!("{}", CountryCode::Jor), "Jordan"); + assert_eq!(format!("{}", CountryCode::Kaz), "Kazakhstan"); + assert_eq!(format!("{}", CountryCode::Ken), "Kenya"); + assert_eq!(format!("{}", CountryCode::Kir), "Kiribati"); + assert_eq!(format!("{}", CountryCode::Kwt), "Kuwait"); + assert_eq!(format!("{}", CountryCode::Kgz), "Kyrgyzstan"); + assert_eq!(format!("{}", CountryCode::Lao), "Laos"); + assert_eq!(format!("{}", CountryCode::Lva), "Latvia"); + assert_eq!(format!("{}", CountryCode::Lbn), "Lebanon"); + assert_eq!(format!("{}", CountryCode::Lso), "Lesotho"); + assert_eq!(format!("{}", CountryCode::Lbr), "Liberia"); + assert_eq!(format!("{}", CountryCode::Lby), "Libya"); + assert_eq!(format!("{}", CountryCode::Lie), "Liechtenstein"); + assert_eq!(format!("{}", CountryCode::Ltu), "Lithuania"); + assert_eq!(format!("{}", CountryCode::Lux), "Luxembourg"); + assert_eq!(format!("{}", CountryCode::Mac), "Macao"); + assert_eq!(format!("{}", CountryCode::Mdg), "Madagascar"); + assert_eq!(format!("{}", CountryCode::Mwi), "Malawi"); + assert_eq!(format!("{}", CountryCode::Mys), "Malaysia"); + assert_eq!(format!("{}", CountryCode::Mdv), "Maldives"); + assert_eq!(format!("{}", CountryCode::Mli), "Mali"); + assert_eq!(format!("{}", CountryCode::Mlt), "Malta"); + assert_eq!(format!("{}", CountryCode::Mhl), "Marshall Islands"); + assert_eq!(format!("{}", CountryCode::Mtq), "Martinique"); + assert_eq!(format!("{}", CountryCode::Mrt), "Mauritania"); + assert_eq!(format!("{}", CountryCode::Mus), "Mauritius"); + assert_eq!(format!("{}", CountryCode::Myt), "Mayotte"); + assert_eq!(format!("{}", CountryCode::Mex), "Mexico"); + assert_eq!(format!("{}", CountryCode::Fsm), "Micronesia"); + assert_eq!(format!("{}", CountryCode::Mda), "Moldova"); + assert_eq!(format!("{}", CountryCode::Mco), "Monaco"); + assert_eq!(format!("{}", CountryCode::Mng), "Mongolia"); + assert_eq!(format!("{}", CountryCode::Mne), "Montenegro"); + assert_eq!(format!("{}", CountryCode::Msr), "Montserrat"); + assert_eq!(format!("{}", CountryCode::Mar), "Morocco"); + assert_eq!(format!("{}", CountryCode::Moz), "Mozambique"); + assert_eq!(format!("{}", CountryCode::Mmr), "Myanmar"); + assert_eq!(format!("{}", CountryCode::Nam), "Namibia"); + assert_eq!(format!("{}", CountryCode::Nru), "Nauru"); + assert_eq!(format!("{}", CountryCode::Npl), "Nepal"); + assert_eq!(format!("{}", CountryCode::Nld), "Netherlands"); + assert_eq!(format!("{}", CountryCode::Ncl), "New Caledonia"); + assert_eq!(format!("{}", CountryCode::Nzl), "New Zealand"); + assert_eq!(format!("{}", CountryCode::Nic), "Nicaragua"); + assert_eq!(format!("{}", CountryCode::Ner), "Niger"); + assert_eq!(format!("{}", CountryCode::Nga), "Nigeria"); + assert_eq!(format!("{}", CountryCode::Niu), "Niue"); + assert_eq!(format!("{}", CountryCode::Nfk), "Norfolk Island"); + assert_eq!(format!("{}", CountryCode::Prk), "North Korea"); + assert_eq!(format!("{}", CountryCode::Mkd), "North Macedonia"); + assert_eq!(format!("{}", CountryCode::Mnp), "Northern Mariana Islands"); + assert_eq!(format!("{}", CountryCode::Nor), "Norway"); + assert_eq!(format!("{}", CountryCode::Omn), "Oman"); + assert_eq!(format!("{}", CountryCode::Pak), "Pakistan"); + assert_eq!(format!("{}", CountryCode::Plw), "Palau"); + assert_eq!(format!("{}", CountryCode::Pse), "Palestine"); + assert_eq!(format!("{}", CountryCode::Pan), "Panama"); + assert_eq!(format!("{}", CountryCode::Png), "Papua New Guinea"); + assert_eq!(format!("{}", CountryCode::Pry), "Paraguay"); + assert_eq!(format!("{}", CountryCode::Per), "Peru"); + assert_eq!(format!("{}", CountryCode::Phl), "Philippines"); + assert_eq!(format!("{}", CountryCode::Pcn), "Pitcairn"); + assert_eq!(format!("{}", CountryCode::Pol), "Poland"); + assert_eq!(format!("{}", CountryCode::Prt), "Portugal"); + assert_eq!(format!("{}", CountryCode::Pri), "Puerto Rico"); + assert_eq!(format!("{}", CountryCode::Qat), "Qatar"); + assert_eq!(format!("{}", CountryCode::Cog), "Republic of the Congo"); + assert_eq!(format!("{}", CountryCode::Reu), "Réunion"); + assert_eq!(format!("{}", CountryCode::Rou), "Romania"); + assert_eq!(format!("{}", CountryCode::Rus), "Russia"); + assert_eq!(format!("{}", CountryCode::Rwa), "Rwanda"); + assert_eq!(format!("{}", CountryCode::Blm), "Saint Barthélemy"); + assert_eq!( + format!("{}", CountryCode::Shn), + "Saint Helena, Ascension and Tristan da Cunha" + ); + assert_eq!(format!("{}", CountryCode::Kna), "Saint Kitts and Nevis"); + assert_eq!(format!("{}", CountryCode::Lca), "Saint Lucia"); + assert_eq!(format!("{}", CountryCode::Maf), "Saint Martin"); + assert_eq!(format!("{}", CountryCode::Spm), "Saint Pierre and Miquelon"); + assert_eq!( + format!("{}", CountryCode::Vct), + "Saint Vincent and the Grenadines" + ); + assert_eq!(format!("{}", CountryCode::Wsm), "Samoa"); + assert_eq!(format!("{}", CountryCode::Smr), "San Marino"); + assert_eq!(format!("{}", CountryCode::Stp), "Sao Tome and Principe"); + assert_eq!(format!("{}", CountryCode::Sau), "Saudi Arabia"); + assert_eq!(format!("{}", CountryCode::Sen), "Senegal"); + assert_eq!(format!("{}", CountryCode::Srb), "Serbia"); + assert_eq!(format!("{}", CountryCode::Syc), "Seychelles"); + assert_eq!(format!("{}", CountryCode::Sle), "Sierra Leone"); + assert_eq!(format!("{}", CountryCode::Sgp), "Singapore"); + assert_eq!(format!("{}", CountryCode::Sxm), "Sint Maarten"); + assert_eq!(format!("{}", CountryCode::Svk), "Slovakia"); + assert_eq!(format!("{}", CountryCode::Svn), "Slovenia"); + assert_eq!(format!("{}", CountryCode::Slb), "Solomon Islands"); + assert_eq!(format!("{}", CountryCode::Som), "Somalia"); + assert_eq!(format!("{}", CountryCode::Zaf), "South Africa"); + assert_eq!( + format!("{}", CountryCode::Sgs), + "South Georgia and the South Sandwich Islands" + ); + assert_eq!(format!("{}", CountryCode::Kor), "South Korea"); + assert_eq!(format!("{}", CountryCode::Ssd), "South Sudan"); + assert_eq!(format!("{}", CountryCode::Esp), "Spain"); + assert_eq!(format!("{}", CountryCode::Lka), "Sri Lanka"); + assert_eq!(format!("{}", CountryCode::Sdn), "Sudan"); + assert_eq!(format!("{}", CountryCode::Sur), "Suriname"); + assert_eq!(format!("{}", CountryCode::Sjm), "Svalbard and Jan Mayen"); + assert_eq!(format!("{}", CountryCode::Swe), "Sweden"); + assert_eq!(format!("{}", CountryCode::Che), "Switzerland"); + assert_eq!(format!("{}", CountryCode::Syr), "Syria"); + assert_eq!(format!("{}", CountryCode::Twn), "Taiwan"); + assert_eq!(format!("{}", CountryCode::Tjk), "Tajikistan"); + assert_eq!(format!("{}", CountryCode::Tza), "Tanzania"); + assert_eq!(format!("{}", CountryCode::Tha), "Thailand"); + assert_eq!(format!("{}", CountryCode::Tls), "Timor-Leste"); + assert_eq!(format!("{}", CountryCode::Tgo), "Togo"); + assert_eq!(format!("{}", CountryCode::Tkl), "Tokelau"); + assert_eq!(format!("{}", CountryCode::Ton), "Tonga"); + assert_eq!(format!("{}", CountryCode::Tto), "Trinidad and Tobago"); + assert_eq!(format!("{}", CountryCode::Tun), "Tunisia"); + assert_eq!(format!("{}", CountryCode::Tur), "Turkey"); + assert_eq!(format!("{}", CountryCode::Tkm), "Turkmenistan"); + assert_eq!(format!("{}", CountryCode::Tca), "Turks and Caicos Islands"); + assert_eq!(format!("{}", CountryCode::Tuv), "Tuvalu"); + assert_eq!(format!("{}", CountryCode::Uga), "Uganda"); + assert_eq!(format!("{}", CountryCode::Ukr), "Ukraine"); + assert_eq!(format!("{}", CountryCode::Are), "United Arab Emirates"); + assert_eq!(format!("{}", CountryCode::Gbr), "United Kingdom"); + assert_eq!( + format!("{}", CountryCode::Umi), + "United States Minor Outlying Islands" + ); + assert_eq!(format!("{}", CountryCode::Usa), "United States of America"); + assert_eq!(format!("{}", CountryCode::Ury), "Uruguay"); + assert_eq!(format!("{}", CountryCode::Uzb), "Uzbekistan"); + assert_eq!(format!("{}", CountryCode::Vut), "Vanuatu"); + assert_eq!(format!("{}", CountryCode::Vat), "Vatican City"); + assert_eq!(format!("{}", CountryCode::Ven), "Venezuela"); + assert_eq!(format!("{}", CountryCode::Vnm), "Viet Nam"); + assert_eq!(format!("{}", CountryCode::Vgb), "Virgin Islands (British)"); + assert_eq!(format!("{}", CountryCode::Vir), "Virgin Islands (U.S.)"); + assert_eq!(format!("{}", CountryCode::Wlf), "Wallis and Futuna"); + assert_eq!(format!("{}", CountryCode::Esh), "Western Sahara"); + assert_eq!(format!("{}", CountryCode::Yem), "Yemen"); + assert_eq!(format!("{}", CountryCode::Zmb), "Zambia"); + assert_eq!(format!("{}", CountryCode::Zwe), "Zimbabwe"); +} + +#[test] +fn test_countrycode_fromstr() { + use std::str::FromStr; + assert_eq!( + CountryCode::from_str("Afghanistan").unwrap(), + CountryCode::Afg + ); + assert_eq!( + CountryCode::from_str("Åland Islands").unwrap(), + CountryCode::Ala + ); + assert_eq!(CountryCode::from_str("Albania").unwrap(), CountryCode::Alb); + assert_eq!(CountryCode::from_str("Algeria").unwrap(), CountryCode::Dza); + assert_eq!( + CountryCode::from_str("American Samoa").unwrap(), + CountryCode::Asm + ); + assert_eq!(CountryCode::from_str("Andorra").unwrap(), CountryCode::And); + assert_eq!(CountryCode::from_str("Angola").unwrap(), CountryCode::Ago); + assert_eq!(CountryCode::from_str("Anguilla").unwrap(), CountryCode::Aia); + assert_eq!( + CountryCode::from_str("Antarctica").unwrap(), + CountryCode::Ata + ); + assert_eq!( + CountryCode::from_str("Antigua and Barbuda").unwrap(), + CountryCode::Atg + ); + assert_eq!( + CountryCode::from_str("Argentina").unwrap(), + CountryCode::Arg + ); + assert_eq!(CountryCode::from_str("Armenia").unwrap(), CountryCode::Arm); + assert_eq!(CountryCode::from_str("Aruba").unwrap(), CountryCode::Abw); + assert_eq!( + CountryCode::from_str("Australia").unwrap(), + CountryCode::Aus + ); + assert_eq!(CountryCode::from_str("Austria").unwrap(), CountryCode::Aut); + assert_eq!( + CountryCode::from_str("Azerbaijan").unwrap(), + CountryCode::Aze + ); + assert_eq!(CountryCode::from_str("Bahamas").unwrap(), CountryCode::Bhs); + assert_eq!(CountryCode::from_str("Bahrain").unwrap(), CountryCode::Bhr); + assert_eq!( + CountryCode::from_str("Bangladesh").unwrap(), + CountryCode::Bgd + ); + assert_eq!(CountryCode::from_str("Barbados").unwrap(), CountryCode::Brb); + assert_eq!(CountryCode::from_str("Belarus").unwrap(), CountryCode::Blr); + assert_eq!(CountryCode::from_str("Belgium").unwrap(), CountryCode::Bel); + assert_eq!(CountryCode::from_str("Belize").unwrap(), CountryCode::Blz); + assert_eq!(CountryCode::from_str("Benin").unwrap(), CountryCode::Ben); + assert_eq!(CountryCode::from_str("Bermuda").unwrap(), CountryCode::Bmu); + assert_eq!(CountryCode::from_str("Bhutan").unwrap(), CountryCode::Btn); + assert_eq!(CountryCode::from_str("Bolivia").unwrap(), CountryCode::Bol); + assert_eq!( + CountryCode::from_str("Bonaire, Sint Eustatius and Saba").unwrap(), + CountryCode::Bes + ); + assert_eq!( + CountryCode::from_str("Bosnia and Herzegovina").unwrap(), + CountryCode::Bih + ); + assert_eq!(CountryCode::from_str("Botswana").unwrap(), CountryCode::Bwa); + assert_eq!( + CountryCode::from_str("Bouvet Island").unwrap(), + CountryCode::Bvt + ); + assert_eq!(CountryCode::from_str("Brazil").unwrap(), CountryCode::Bra); + assert_eq!( + CountryCode::from_str("British Indian Ocean Territory").unwrap(), + CountryCode::Iot + ); + assert_eq!(CountryCode::from_str("Brunei").unwrap(), CountryCode::Brn); + assert_eq!(CountryCode::from_str("Bulgaria").unwrap(), CountryCode::Bgr); + assert_eq!( + CountryCode::from_str("Burkina Faso").unwrap(), + CountryCode::Bfa + ); + assert_eq!(CountryCode::from_str("Burundi").unwrap(), CountryCode::Bdi); + assert_eq!( + CountryCode::from_str("Cabo Verde").unwrap(), + CountryCode::Cpv + ); + assert_eq!(CountryCode::from_str("Cambodia").unwrap(), CountryCode::Khm); + assert_eq!(CountryCode::from_str("Cameroon").unwrap(), CountryCode::Cmr); + assert_eq!(CountryCode::from_str("Canada").unwrap(), CountryCode::Can); + assert_eq!( + CountryCode::from_str("Cayman Islands").unwrap(), + CountryCode::Cym + ); + assert_eq!( + CountryCode::from_str("Central African Republic").unwrap(), + CountryCode::Caf + ); + assert_eq!(CountryCode::from_str("Chad").unwrap(), CountryCode::Tcd); + assert_eq!(CountryCode::from_str("Chile").unwrap(), CountryCode::Chl); + assert_eq!(CountryCode::from_str("China").unwrap(), CountryCode::Chn); + assert_eq!( + CountryCode::from_str("Christmas Island").unwrap(), + CountryCode::Cxr + ); + assert_eq!( + CountryCode::from_str("Cocos (Keeling) Islands").unwrap(), + CountryCode::Cck + ); + assert_eq!(CountryCode::from_str("Colombia").unwrap(), CountryCode::Col); + assert_eq!(CountryCode::from_str("Comoros").unwrap(), CountryCode::Com); + assert_eq!( + CountryCode::from_str("Cook Islands").unwrap(), + CountryCode::Cok + ); + assert_eq!( + CountryCode::from_str("Costa Rica").unwrap(), + CountryCode::Cri + ); + assert_eq!( + CountryCode::from_str("Côte d'Ivoire").unwrap(), + CountryCode::Civ + ); + assert_eq!(CountryCode::from_str("Croatia").unwrap(), CountryCode::Hrv); + assert_eq!(CountryCode::from_str("Cuba").unwrap(), CountryCode::Cub); + assert_eq!(CountryCode::from_str("Curaçao").unwrap(), CountryCode::Cuw); + assert_eq!(CountryCode::from_str("Cyprus").unwrap(), CountryCode::Cyp); + assert_eq!(CountryCode::from_str("Czechia").unwrap(), CountryCode::Cze); + assert_eq!( + CountryCode::from_str("Democratic Republic of the Congo").unwrap(), + CountryCode::Cod + ); + assert_eq!(CountryCode::from_str("Denmark").unwrap(), CountryCode::Dnk); + assert_eq!(CountryCode::from_str("Djibouti").unwrap(), CountryCode::Dji); + assert_eq!(CountryCode::from_str("Dominica").unwrap(), CountryCode::Dma); + assert_eq!( + CountryCode::from_str("Dominican Republic").unwrap(), + CountryCode::Dom + ); + assert_eq!(CountryCode::from_str("Ecuador").unwrap(), CountryCode::Ecu); + assert_eq!(CountryCode::from_str("Egypt").unwrap(), CountryCode::Egy); + assert_eq!( + CountryCode::from_str("El Salvador").unwrap(), + CountryCode::Slv + ); + assert_eq!( + CountryCode::from_str("Equatorial Guinea").unwrap(), + CountryCode::Gnq + ); + assert_eq!(CountryCode::from_str("Eritrea").unwrap(), CountryCode::Eri); + assert_eq!(CountryCode::from_str("Estonia").unwrap(), CountryCode::Est); + assert_eq!(CountryCode::from_str("Eswatini").unwrap(), CountryCode::Swz); + assert_eq!(CountryCode::from_str("Ethiopia").unwrap(), CountryCode::Eth); + assert_eq!( + CountryCode::from_str("Falkland Islands").unwrap(), + CountryCode::Flk + ); + assert_eq!( + CountryCode::from_str("Faroe Islands").unwrap(), + CountryCode::Fro + ); + assert_eq!(CountryCode::from_str("Fiji").unwrap(), CountryCode::Fji); + assert_eq!(CountryCode::from_str("Finland").unwrap(), CountryCode::Fin); + assert_eq!(CountryCode::from_str("France").unwrap(), CountryCode::Fra); + assert_eq!( + CountryCode::from_str("French Guiana").unwrap(), + CountryCode::Guf + ); + assert_eq!( + CountryCode::from_str("French Polynesia").unwrap(), + CountryCode::Pyf + ); + assert_eq!( + CountryCode::from_str("French Southern Territories").unwrap(), + CountryCode::Atf + ); + assert_eq!(CountryCode::from_str("Gabon").unwrap(), CountryCode::Gab); + assert_eq!(CountryCode::from_str("Gambia").unwrap(), CountryCode::Gmb); + assert_eq!(CountryCode::from_str("Georgia").unwrap(), CountryCode::Geo); + assert_eq!(CountryCode::from_str("Germany").unwrap(), CountryCode::Deu); + assert_eq!(CountryCode::from_str("Ghana").unwrap(), CountryCode::Gha); + assert_eq!( + CountryCode::from_str("Gibraltar").unwrap(), + CountryCode::Gib + ); + assert_eq!(CountryCode::from_str("Greece").unwrap(), CountryCode::Grc); + assert_eq!( + CountryCode::from_str("Greenland").unwrap(), + CountryCode::Grl + ); + assert_eq!(CountryCode::from_str("Grenada").unwrap(), CountryCode::Grd); + assert_eq!( + CountryCode::from_str("Guadeloupe").unwrap(), + CountryCode::Glp + ); + assert_eq!(CountryCode::from_str("Guam").unwrap(), CountryCode::Gum); + assert_eq!( + CountryCode::from_str("Guatemala").unwrap(), + CountryCode::Gtm + ); + assert_eq!(CountryCode::from_str("Guernsey").unwrap(), CountryCode::Ggy); + assert_eq!(CountryCode::from_str("Guinea").unwrap(), CountryCode::Gin); + assert_eq!( + CountryCode::from_str("Guinea-Bissau").unwrap(), + CountryCode::Gnb + ); + assert_eq!(CountryCode::from_str("Guyana").unwrap(), CountryCode::Guy); + assert_eq!(CountryCode::from_str("Haiti").unwrap(), CountryCode::Hti); + assert_eq!( + CountryCode::from_str("Heard Island and McDonald Islands").unwrap(), + CountryCode::Hmd + ); + assert_eq!(CountryCode::from_str("Honduras").unwrap(), CountryCode::Hnd); + assert_eq!( + CountryCode::from_str("Hong Kong").unwrap(), + CountryCode::Hkg + ); + assert_eq!(CountryCode::from_str("Hungary").unwrap(), CountryCode::Hun); + assert_eq!(CountryCode::from_str("Iceland").unwrap(), CountryCode::Isl); + assert_eq!(CountryCode::from_str("India").unwrap(), CountryCode::Ind); + assert_eq!( + CountryCode::from_str("Indonesia").unwrap(), + CountryCode::Idn + ); + assert_eq!(CountryCode::from_str("Iran").unwrap(), CountryCode::Irn); + assert_eq!(CountryCode::from_str("Iraq").unwrap(), CountryCode::Irq); + assert_eq!(CountryCode::from_str("Ireland").unwrap(), CountryCode::Irl); + assert_eq!( + CountryCode::from_str("Isle of Man").unwrap(), + CountryCode::Imn + ); + assert_eq!(CountryCode::from_str("Israel").unwrap(), CountryCode::Isr); + assert_eq!(CountryCode::from_str("Italy").unwrap(), CountryCode::Ita); + assert_eq!(CountryCode::from_str("Jamaica").unwrap(), CountryCode::Jam); + assert_eq!(CountryCode::from_str("Japan").unwrap(), CountryCode::Jpn); + assert_eq!(CountryCode::from_str("Jersey").unwrap(), CountryCode::Jey); + assert_eq!(CountryCode::from_str("Jordan").unwrap(), CountryCode::Jor); + assert_eq!( + CountryCode::from_str("Kazakhstan").unwrap(), + CountryCode::Kaz + ); + assert_eq!(CountryCode::from_str("Kenya").unwrap(), CountryCode::Ken); + assert_eq!(CountryCode::from_str("Kiribati").unwrap(), CountryCode::Kir); + assert_eq!(CountryCode::from_str("Kuwait").unwrap(), CountryCode::Kwt); + assert_eq!( + CountryCode::from_str("Kyrgyzstan").unwrap(), + CountryCode::Kgz + ); + assert_eq!(CountryCode::from_str("Laos").unwrap(), CountryCode::Lao); + assert_eq!(CountryCode::from_str("Latvia").unwrap(), CountryCode::Lva); + assert_eq!(CountryCode::from_str("Lebanon").unwrap(), CountryCode::Lbn); + assert_eq!(CountryCode::from_str("Lesotho").unwrap(), CountryCode::Lso); + assert_eq!(CountryCode::from_str("Liberia").unwrap(), CountryCode::Lbr); + assert_eq!(CountryCode::from_str("Libya").unwrap(), CountryCode::Lby); + assert_eq!( + CountryCode::from_str("Liechtenstein").unwrap(), + CountryCode::Lie + ); + assert_eq!( + CountryCode::from_str("Lithuania").unwrap(), + CountryCode::Ltu + ); + assert_eq!( + CountryCode::from_str("Luxembourg").unwrap(), + CountryCode::Lux + ); + assert_eq!(CountryCode::from_str("Macao").unwrap(), CountryCode::Mac); + assert_eq!( + CountryCode::from_str("Madagascar").unwrap(), + CountryCode::Mdg + ); + assert_eq!(CountryCode::from_str("Malawi").unwrap(), CountryCode::Mwi); + assert_eq!(CountryCode::from_str("Malaysia").unwrap(), CountryCode::Mys); + assert_eq!(CountryCode::from_str("Maldives").unwrap(), CountryCode::Mdv); + assert_eq!(CountryCode::from_str("Mali").unwrap(), CountryCode::Mli); + assert_eq!(CountryCode::from_str("Malta").unwrap(), CountryCode::Mlt); + assert_eq!( + CountryCode::from_str("Marshall Islands").unwrap(), + CountryCode::Mhl + ); + assert_eq!( + CountryCode::from_str("Martinique").unwrap(), + CountryCode::Mtq + ); + assert_eq!( + CountryCode::from_str("Mauritania").unwrap(), + CountryCode::Mrt + ); + assert_eq!( + CountryCode::from_str("Mauritius").unwrap(), + CountryCode::Mus + ); + assert_eq!(CountryCode::from_str("Mayotte").unwrap(), CountryCode::Myt); + assert_eq!(CountryCode::from_str("Mexico").unwrap(), CountryCode::Mex); + assert_eq!( + CountryCode::from_str("Micronesia").unwrap(), + CountryCode::Fsm + ); + assert_eq!(CountryCode::from_str("Moldova").unwrap(), CountryCode::Mda); + assert_eq!(CountryCode::from_str("Monaco").unwrap(), CountryCode::Mco); + assert_eq!(CountryCode::from_str("Mongolia").unwrap(), CountryCode::Mng); + assert_eq!( + CountryCode::from_str("Montenegro").unwrap(), + CountryCode::Mne + ); + assert_eq!( + CountryCode::from_str("Montserrat").unwrap(), + CountryCode::Msr + ); + assert_eq!(CountryCode::from_str("Morocco").unwrap(), CountryCode::Mar); + assert_eq!( + CountryCode::from_str("Mozambique").unwrap(), + CountryCode::Moz + ); + assert_eq!(CountryCode::from_str("Myanmar").unwrap(), CountryCode::Mmr); + assert_eq!(CountryCode::from_str("Namibia").unwrap(), CountryCode::Nam); + assert_eq!(CountryCode::from_str("Nauru").unwrap(), CountryCode::Nru); + assert_eq!(CountryCode::from_str("Nepal").unwrap(), CountryCode::Npl); + assert_eq!( + CountryCode::from_str("Netherlands").unwrap(), + CountryCode::Nld + ); + assert_eq!( + CountryCode::from_str("New Caledonia").unwrap(), + CountryCode::Ncl + ); + assert_eq!( + CountryCode::from_str("New Zealand").unwrap(), + CountryCode::Nzl + ); + assert_eq!( + CountryCode::from_str("Nicaragua").unwrap(), + CountryCode::Nic + ); + assert_eq!(CountryCode::from_str("Niger").unwrap(), CountryCode::Ner); + assert_eq!(CountryCode::from_str("Nigeria").unwrap(), CountryCode::Nga); + assert_eq!(CountryCode::from_str("Niue").unwrap(), CountryCode::Niu); + assert_eq!( + CountryCode::from_str("Norfolk Island").unwrap(), + CountryCode::Nfk + ); + assert_eq!( + CountryCode::from_str("North Korea").unwrap(), + CountryCode::Prk + ); + assert_eq!( + CountryCode::from_str("North Macedonia").unwrap(), + CountryCode::Mkd + ); + assert_eq!( + CountryCode::from_str("Northern Mariana Islands").unwrap(), + CountryCode::Mnp + ); + assert_eq!(CountryCode::from_str("Norway").unwrap(), CountryCode::Nor); + assert_eq!(CountryCode::from_str("Oman").unwrap(), CountryCode::Omn); + assert_eq!(CountryCode::from_str("Pakistan").unwrap(), CountryCode::Pak); + assert_eq!(CountryCode::from_str("Palau").unwrap(), CountryCode::Plw); + assert_eq!( + CountryCode::from_str("Palestine").unwrap(), + CountryCode::Pse + ); + assert_eq!(CountryCode::from_str("Panama").unwrap(), CountryCode::Pan); + assert_eq!( + CountryCode::from_str("Papua New Guinea").unwrap(), + CountryCode::Png + ); + assert_eq!(CountryCode::from_str("Paraguay").unwrap(), CountryCode::Pry); + assert_eq!(CountryCode::from_str("Peru").unwrap(), CountryCode::Per); + assert_eq!( + CountryCode::from_str("Philippines").unwrap(), + CountryCode::Phl + ); + assert_eq!(CountryCode::from_str("Pitcairn").unwrap(), CountryCode::Pcn); + assert_eq!(CountryCode::from_str("Poland").unwrap(), CountryCode::Pol); + assert_eq!(CountryCode::from_str("Portugal").unwrap(), CountryCode::Prt); + assert_eq!( + CountryCode::from_str("Puerto Rico").unwrap(), + CountryCode::Pri + ); + assert_eq!(CountryCode::from_str("Qatar").unwrap(), CountryCode::Qat); + assert_eq!( + CountryCode::from_str("Republic of the Congo").unwrap(), + CountryCode::Cog + ); + assert_eq!(CountryCode::from_str("Réunion").unwrap(), CountryCode::Reu); + assert_eq!(CountryCode::from_str("Romania").unwrap(), CountryCode::Rou); + assert_eq!(CountryCode::from_str("Russia").unwrap(), CountryCode::Rus); + assert_eq!(CountryCode::from_str("Rwanda").unwrap(), CountryCode::Rwa); + assert_eq!( + CountryCode::from_str("Saint Barthélemy").unwrap(), + CountryCode::Blm + ); + assert_eq!( + CountryCode::from_str("Saint Helena, Ascension and Tristan da Cunha").unwrap(), + CountryCode::Shn + ); + assert_eq!( + CountryCode::from_str("Saint Kitts and Nevis").unwrap(), + CountryCode::Kna + ); + assert_eq!( + CountryCode::from_str("Saint Lucia").unwrap(), + CountryCode::Lca + ); + assert_eq!( + CountryCode::from_str("Saint Martin").unwrap(), + CountryCode::Maf + ); + assert_eq!( + CountryCode::from_str("Saint Pierre and Miquelon").unwrap(), + CountryCode::Spm + ); + assert_eq!( + CountryCode::from_str("Saint Vincent and the Grenadines").unwrap(), + CountryCode::Vct + ); + assert_eq!(CountryCode::from_str("Samoa").unwrap(), CountryCode::Wsm); + assert_eq!( + CountryCode::from_str("San Marino").unwrap(), + CountryCode::Smr + ); + assert_eq!( + CountryCode::from_str("Sao Tome and Principe").unwrap(), + CountryCode::Stp + ); + assert_eq!( + CountryCode::from_str("Saudi Arabia").unwrap(), + CountryCode::Sau + ); + assert_eq!(CountryCode::from_str("Senegal").unwrap(), CountryCode::Sen); + assert_eq!(CountryCode::from_str("Serbia").unwrap(), CountryCode::Srb); + assert_eq!( + CountryCode::from_str("Seychelles").unwrap(), + CountryCode::Syc + ); + assert_eq!( + CountryCode::from_str("Sierra Leone").unwrap(), + CountryCode::Sle + ); + assert_eq!( + CountryCode::from_str("Singapore").unwrap(), + CountryCode::Sgp + ); + assert_eq!( + CountryCode::from_str("Sint Maarten").unwrap(), + CountryCode::Sxm + ); + assert_eq!(CountryCode::from_str("Slovakia").unwrap(), CountryCode::Svk); + assert_eq!(CountryCode::from_str("Slovenia").unwrap(), CountryCode::Svn); + assert_eq!( + CountryCode::from_str("Solomon Islands").unwrap(), + CountryCode::Slb + ); + assert_eq!(CountryCode::from_str("Somalia").unwrap(), CountryCode::Som); + assert_eq!( + CountryCode::from_str("South Africa").unwrap(), + CountryCode::Zaf + ); + assert_eq!( + CountryCode::from_str("South Georgia and the South Sandwich Islands").unwrap(), + CountryCode::Sgs + ); + assert_eq!( + CountryCode::from_str("South Korea").unwrap(), + CountryCode::Kor + ); + assert_eq!( + CountryCode::from_str("South Sudan").unwrap(), + CountryCode::Ssd + ); + assert_eq!(CountryCode::from_str("Spain").unwrap(), CountryCode::Esp); + assert_eq!( + CountryCode::from_str("Sri Lanka").unwrap(), + CountryCode::Lka + ); + assert_eq!(CountryCode::from_str("Sudan").unwrap(), CountryCode::Sdn); + assert_eq!(CountryCode::from_str("Suriname").unwrap(), CountryCode::Sur); + assert_eq!( + CountryCode::from_str("Svalbard and Jan Mayen").unwrap(), + CountryCode::Sjm + ); + assert_eq!(CountryCode::from_str("Sweden").unwrap(), CountryCode::Swe); + assert_eq!( + CountryCode::from_str("Switzerland").unwrap(), + CountryCode::Che + ); + assert_eq!(CountryCode::from_str("Syria").unwrap(), CountryCode::Syr); + assert_eq!(CountryCode::from_str("Taiwan").unwrap(), CountryCode::Twn); + assert_eq!( + CountryCode::from_str("Tajikistan").unwrap(), + CountryCode::Tjk + ); + assert_eq!(CountryCode::from_str("Tanzania").unwrap(), CountryCode::Tza); + assert_eq!(CountryCode::from_str("Thailand").unwrap(), CountryCode::Tha); + assert_eq!( + CountryCode::from_str("Timor-Leste").unwrap(), + CountryCode::Tls + ); + assert_eq!(CountryCode::from_str("Togo").unwrap(), CountryCode::Tgo); + assert_eq!(CountryCode::from_str("Tokelau").unwrap(), CountryCode::Tkl); + assert_eq!(CountryCode::from_str("Tonga").unwrap(), CountryCode::Ton); + assert_eq!( + CountryCode::from_str("Trinidad and Tobago").unwrap(), + CountryCode::Tto + ); + assert_eq!(CountryCode::from_str("Tunisia").unwrap(), CountryCode::Tun); + assert_eq!(CountryCode::from_str("Turkey").unwrap(), CountryCode::Tur); + assert_eq!( + CountryCode::from_str("Turkmenistan").unwrap(), + CountryCode::Tkm + ); + assert_eq!( + CountryCode::from_str("Turks and Caicos Islands").unwrap(), + CountryCode::Tca + ); + assert_eq!(CountryCode::from_str("Tuvalu").unwrap(), CountryCode::Tuv); + assert_eq!(CountryCode::from_str("Uganda").unwrap(), CountryCode::Uga); + assert_eq!(CountryCode::from_str("Ukraine").unwrap(), CountryCode::Ukr); + assert_eq!( + CountryCode::from_str("United Arab Emirates").unwrap(), + CountryCode::Are + ); + assert_eq!( + CountryCode::from_str("United Kingdom").unwrap(), + CountryCode::Gbr + ); + assert_eq!( + CountryCode::from_str("United States Minor Outlying Islands").unwrap(), + CountryCode::Umi + ); + assert_eq!( + CountryCode::from_str("United States of America").unwrap(), + CountryCode::Usa + ); + assert_eq!(CountryCode::from_str("Uruguay").unwrap(), CountryCode::Ury); + assert_eq!( + CountryCode::from_str("Uzbekistan").unwrap(), + CountryCode::Uzb + ); + assert_eq!(CountryCode::from_str("Vanuatu").unwrap(), CountryCode::Vut); + assert_eq!( + CountryCode::from_str("Vatican City").unwrap(), + CountryCode::Vat + ); + assert_eq!( + CountryCode::from_str("Venezuela").unwrap(), + CountryCode::Ven + ); + assert_eq!(CountryCode::from_str("Viet Nam").unwrap(), CountryCode::Vnm); + assert_eq!( + CountryCode::from_str("Virgin Islands (British)").unwrap(), + CountryCode::Vgb + ); + assert_eq!( + CountryCode::from_str("Virgin Islands (U.S.)").unwrap(), + CountryCode::Vir + ); + assert_eq!( + CountryCode::from_str("Wallis and Futuna").unwrap(), + CountryCode::Wlf + ); + assert_eq!( + CountryCode::from_str("Western Sahara").unwrap(), + CountryCode::Esh + ); + assert_eq!(CountryCode::from_str("Yemen").unwrap(), CountryCode::Yem); + assert_eq!(CountryCode::from_str("Zambia").unwrap(), CountryCode::Zmb); + assert_eq!(CountryCode::from_str("Zimbabwe").unwrap(), CountryCode::Zwe); + assert!(CountryCode::from_str("Narnia").is_err()); + assert!(CountryCode::from_str("Mesopotamia").is_err()); + assert!(CountryCode::from_str("Czechoslovakia").is_err()); +} + +#[cfg(feature = "backend")] +pub mod crud; diff --git a/thoth-api/src/model/mod.rs b/thoth-api/src/model/mod.rs index 88bbceaa..f025bb0b 100644 --- a/thoth-api/src/model/mod.rs +++ b/thoth-api/src/model/mod.rs @@ -11,6 +11,7 @@ use uuid::Uuid; pub const DOI_DOMAIN: &str = "https://doi.org/"; pub const ORCID_DOMAIN: &str = "https://orcid.org/"; +pub const ROR_DOMAIN: &str = "https://ror.org/"; #[cfg_attr( feature = "backend", @@ -56,6 +57,16 @@ pub struct Isbn(String); #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Orcid(String); +#[cfg_attr( + feature = "backend", + derive(DieselNewType, juniper::GraphQLScalarValue), + graphql( + description = r#"ROR (Research Organization Registry) identifier. Expressed as `^https:\/\/ror\.org\/0[a-hjkmnp-z0-9]{6}\d{2}$`"# + ) +)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Ror(String); + #[cfg_attr( feature = "backend", derive(DieselNewType, juniper::GraphQLScalarValue), @@ -88,6 +99,12 @@ impl Default for Orcid { } } +impl Default for Ror { + fn default() -> Ror { + Ror(Default::default()) + } +} + impl Default for Timestamp { fn default() -> Timestamp { Timestamp(TimeZone::timestamp(&Utc, 0, 0)) @@ -112,6 +129,12 @@ impl fmt::Display for Orcid { } } +impl fmt::Display for Ror { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", &self.0.replace(ROR_DOMAIN, "")) + } +} + impl fmt::Display for Timestamp { fn fmt(&self, f: &mut std::fmt::Formatter) -> fmt::Result { write!(f, "{}", &self.0.format("%F %T")) @@ -205,6 +228,40 @@ impl FromStr for Orcid { } } +impl FromStr for Ror { + type Err = ThothError; + + fn from_str(input: &str) -> ThothResult { + use lazy_static::lazy_static; + use regex::Regex; + lazy_static! { + static ref RE: Regex = Regex::new( + // ^ = beginning of string + // (?:) = non-capturing group + // i = case-insensitive flag + // $ = end of string + // Matches strings of format "[[[http[s]://]|[https://www.]]ror.org/]0XXXXXXNN" + // and captures the 7-character/2-digit-checksum identifier segment + // Corresponds to database constraints although regex syntax differs slightly + r#"^(?i:(?:https?://|https://www\.)?ror\.org/)?(0[a-hjkmnp-z0-9]{6}\d{2}$)"#).unwrap(); + } + if input.is_empty() { + Err(ThothError::RorEmptyError) + } else if let Some(matches) = RE.captures(input) { + // The 0th capture always corresponds to the entire match + if let Some(identifier) = matches.get(1) { + let standardised = format!("{}{}", ROR_DOMAIN, identifier.as_str()); + let ror: Ror = Ror(standardised); + Ok(ror) + } else { + Err(ThothError::RorParseError(input.to_string())) + } + } else { + Err(ThothError::RorParseError(input.to_string())) + } + } +} + impl Doi { pub fn to_lowercase_string(&self) -> String { self.0.to_lowercase() @@ -477,6 +534,12 @@ fn test_orcid_default() { assert_eq!(orcid, Orcid("".to_string())); } +#[test] +fn test_ror_default() { + let ror: Ror = Default::default(); + assert_eq!(ror, Ror("".to_string())); +} + #[test] fn test_timestamp_default() { let stamp: Timestamp = Default::default(); @@ -501,6 +564,12 @@ fn test_orcid_display() { assert_eq!(format!("{}", orcid), "0000-0002-1234-5678"); } +#[test] +fn test_ror_display() { + let ror = Ror("https://ror.org/0abcdef12".to_string()); + assert_eq!(format!("{}", ror), "0abcdef12"); +} + #[test] fn test_timestamp_display() { let stamp: Timestamp = Default::default(); @@ -653,6 +722,45 @@ fn test_orcid_fromstr() { assert!(Orcid::from_str("0000-0002-1234-5678https://orcid.org/").is_err()); } +#[test] +fn test_ror_fromstr() { + let standardised = Ror("https://ror.org/0abcdef12".to_string()); + assert_eq!( + Ror::from_str("https://ror.org/0abcdef12").unwrap(), + standardised + ); + assert_eq!( + Ror::from_str("http://ror.org/0abcdef12").unwrap(), + standardised + ); + assert_eq!(Ror::from_str("ror.org/0abcdef12").unwrap(), standardised); + assert_eq!(Ror::from_str("0abcdef12").unwrap(), standardised); + assert_eq!( + Ror::from_str("HTTPS://ROR.ORG/0abcdef12").unwrap(), + standardised + ); + assert_eq!( + Ror::from_str("Https://Ror.org/0abcdef12").unwrap(), + standardised + ); + assert_eq!( + Ror::from_str("https://www.ror.org/0abcdef12").unwrap(), + standardised + ); + // Testing shows that while leading http://ror and https://www.ror + // resolve successfully, leading www.ror and http://www.ror do not. + assert!(Ror::from_str("http://www.ror.org/0abcdef12").is_err()); + assert!(Ror::from_str("www.ror.org/0abcdef12").is_err()); + assert!(Ror::from_str("htts://ror.org/0abcdef12").is_err()); + assert!(Ror::from_str("https://0abcdef12").is_err()); + assert!(Ror::from_str("https://test.org/0abcdef12").is_err()); + assert!(Ror::from_str("http://test.org/0abcdef12").is_err()); + assert!(Ror::from_str("test.org/0abcdef12").is_err()); + assert!(Ror::from_str("//ror.org/0abcdef12").is_err()); + assert!(Ror::from_str("https://ror-org/0abcdef12").is_err()); + assert!(Ror::from_str("0abcdef12https://ror.org/").is_err()); +} + #[test] // Float equality comparison is fine here because the floats // have already been rounded by the functions under test @@ -729,11 +837,12 @@ fn test_convert_units_from_to() { ); } +pub mod affiliation; pub mod contribution; pub mod contributor; -pub mod funder; pub mod funding; pub mod imprint; +pub mod institution; pub mod issue; pub mod language; pub mod location; diff --git a/thoth-api/src/model/work/mod.rs b/thoth-api/src/model/work/mod.rs index a206d991..3e8d5748 100644 --- a/thoth-api/src/model/work/mod.rs +++ b/thoth-api/src/model/work/mod.rs @@ -6,7 +6,7 @@ use uuid::Uuid; use crate::graphql::utils::Direction; use crate::model::contribution::Contribution; -use crate::model::funding::FundingWithFunder; +use crate::model::funding::FundingWithInstitution; use crate::model::imprint::ImprintWithPublisher; use crate::model::issue::IssueWithSeries; use crate::model::language::Language; @@ -190,7 +190,7 @@ pub struct WorkWithRelations { pub contributions: Option>, pub publications: Option>, pub languages: Option>, - pub fundings: Option>, + pub fundings: Option>, pub subjects: Option>, pub issues: Option>, pub imprint: ImprintWithPublisher, diff --git a/thoth-api/src/schema.rs b/thoth-api/src/schema.rs index 07fc13b8..6ff9b5fc 100644 --- a/thoth-api/src/schema.rs +++ b/thoth-api/src/schema.rs @@ -17,6 +17,32 @@ table! { } } +table! { + use diesel::sql_types::*; + + affiliation (affiliation_id) { + affiliation_id -> Uuid, + contribution_id -> Uuid, + institution_id -> Uuid, + affiliation_ordinal -> Int4, + position -> Nullable, + created_at -> Timestamptz, + updated_at -> Timestamptz, + } +} + +table! { + use diesel::sql_types::*; + + affiliation_history (affiliation_history_id) { + affiliation_history_id -> Uuid, + affiliation_id -> Uuid, + account_id -> Uuid, + data -> Jsonb, + timestamp -> Timestamptz, + } +} + table! { use diesel::sql_types::*; use crate::model::contribution::Contribution_type; @@ -28,7 +54,6 @@ table! { contribution_type -> Contribution_type, main_contribution -> Bool, biography -> Nullable, - institution -> Nullable, created_at -> Timestamptz, updated_at -> Timestamptz, first_name -> Nullable, @@ -77,37 +102,13 @@ table! { } } -table! { - use diesel::sql_types::*; - - funder (funder_id) { - funder_id -> Uuid, - funder_name -> Text, - funder_doi -> Nullable, - created_at -> Timestamptz, - updated_at -> Timestamptz, - } -} - -table! { - use diesel::sql_types::*; - - funder_history (funder_history_id) { - funder_history_id -> Uuid, - funder_id -> Uuid, - account_id -> Uuid, - data -> Jsonb, - timestamp -> Timestamptz, - } -} - table! { use diesel::sql_types::*; funding (funding_id) { funding_id -> Uuid, work_id -> Uuid, - funder_id -> Uuid, + institution_id -> Uuid, program -> Nullable, project_name -> Nullable, project_shortname -> Nullable, @@ -155,6 +156,33 @@ table! { } } +table! { +use diesel::sql_types::*; + use crate::model::institution::Country_code; + + institution (institution_id) { + institution_id -> Uuid, + institution_name -> Text, + institution_doi -> Nullable, + created_at -> Timestamptz, + updated_at -> Timestamptz, + ror -> Nullable, + country_code -> Nullable, + } +} + +table! { + use diesel::sql_types::*; + + institution_history (institution_history_id) { + institution_history_id -> Uuid, + institution_id -> Uuid, + account_id -> Uuid, + data -> Jsonb, + timestamp -> Timestamptz, + } +} + table! { use diesel::sql_types::*; @@ -435,21 +463,25 @@ table! { } } +joinable!(affiliation -> contribution (contribution_id)); +joinable!(affiliation -> institution (institution_id)); +joinable!(affiliation_history -> account (account_id)); +joinable!(affiliation_history -> affiliation (affiliation_id)); joinable!(contribution -> contributor (contributor_id)); joinable!(contribution -> work (work_id)); joinable!(contribution_history -> account (account_id)); joinable!(contribution_history -> contribution (contribution_id)); joinable!(contributor_history -> account (account_id)); joinable!(contributor_history -> contributor (contributor_id)); -joinable!(funder_history -> account (account_id)); -joinable!(funder_history -> funder (funder_id)); -joinable!(funding -> funder (funder_id)); +joinable!(funding -> institution (institution_id)); joinable!(funding -> work (work_id)); joinable!(funding_history -> account (account_id)); joinable!(funding_history -> funding (funding_id)); joinable!(imprint -> publisher (publisher_id)); joinable!(imprint_history -> account (account_id)); joinable!(imprint_history -> imprint (imprint_id)); +joinable!(institution_history -> account (account_id)); +joinable!(institution_history -> institution (institution_id)); joinable!(issue -> series (series_id)); joinable!(issue -> work (work_id)); joinable!(issue_history -> account (account_id)); @@ -482,16 +514,18 @@ joinable!(work_history -> work (work_id)); allow_tables_to_appear_in_same_query!( account, + affiliation, + affiliation_history, contribution, contribution_history, contributor, contributor_history, - funder, - funder_history, funding, funding_history, imprint, imprint_history, + institution, + institution_history, issue, issue_history, language, diff --git a/thoth-app-server/Cargo.toml b/thoth-app-server/Cargo.toml index 7432a7b6..517c2987 100644 --- a/thoth-app-server/Cargo.toml +++ b/thoth-app-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-app-server" -version = "0.5.0" +version = "0.6.0" authors = ["Javier Arias ", "Ross Higman "] edition = "2018" license = "Apache-2.0" diff --git a/thoth-app/Cargo.toml b/thoth-app/Cargo.toml index 4d944524..b5e50c04 100644 --- a/thoth-app/Cargo.toml +++ b/thoth-app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth-app" -version = "0.5.0" +version = "0.6.0" 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.5.0", path = "../thoth-api" } -thoth-errors = { version = "0.5.0", path = "../thoth-errors" } +thoth-api = { version = "0.6.0", path = "../thoth-api" } +thoth-errors = { version = "0.6.0", path = "../thoth-errors" } diff --git a/thoth-app/manifest.json b/thoth-app/manifest.json index 3c55a5c1..dc2c2656 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.5.0", + "version": "0.6.0", "icons": [ { "src": "\/android-icon-36x36.png", diff --git a/thoth-app/src/agent/funder_activity_checker.rs b/thoth-app/src/agent/institution_activity_checker.rs similarity index 56% rename from thoth-app/src/agent/funder_activity_checker.rs rename to thoth-app/src/agent/institution_activity_checker.rs index 0e270eba..6e6c99ad 100644 --- a/thoth-app/src/agent/funder_activity_checker.rs +++ b/thoth-app/src/agent/institution_activity_checker.rs @@ -11,38 +11,38 @@ use crate::agent::notification_bus::NotificationBus; use crate::agent::notification_bus::NotificationDispatcher; use crate::agent::notification_bus::NotificationStatus; use crate::agent::notification_bus::Request as NotificationRequest; -use crate::models::funder::funder_activity_query::FetchActionFunderActivity; -use crate::models::funder::funder_activity_query::FetchFunderActivity; -use crate::models::funder::funder_activity_query::FunderActivityRequest; -use crate::models::funder::funder_activity_query::FunderActivityRequestBody; -use crate::models::funder::funder_activity_query::FunderActivityResponseData; -use crate::models::funder::funder_activity_query::Variables; +use crate::models::institution::institution_activity_query::FetchActionInstitutionActivity; +use crate::models::institution::institution_activity_query::FetchInstitutionActivity; +use crate::models::institution::institution_activity_query::InstitutionActivityRequest; +use crate::models::institution::institution_activity_query::InstitutionActivityRequestBody; +use crate::models::institution::institution_activity_query::InstitutionActivityResponseData; +use crate::models::institution::institution_activity_query::Variables; pub enum Msg { - SetFunderActivityFetchState(FetchActionFunderActivity), + SetInstitutionActivityFetchState(FetchActionInstitutionActivity), } pub enum Request { - RetrieveFunderActivity(Uuid), + RetrieveInstitutionActivity(Uuid), } -pub struct FunderActivityChecker { - agent_link: AgentLink, - fetch_funder_activity: FetchFunderActivity, +pub struct InstitutionActivityChecker { + agent_link: AgentLink, + fetch_institution_activity: FetchInstitutionActivity, subscribers: HashSet, notification_bus: NotificationDispatcher, } -impl Agent for FunderActivityChecker { +impl Agent for InstitutionActivityChecker { type Input = Request; type Message = Msg; - type Output = FunderActivityResponseData; + type Output = InstitutionActivityResponseData; type Reach = Context; fn create(link: AgentLink) -> Self { Self { agent_link: link, - fetch_funder_activity: Default::default(), + fetch_institution_activity: Default::default(), subscribers: HashSet::new(), notification_bus: NotificationBus::dispatcher(), } @@ -50,9 +50,9 @@ impl Agent for FunderActivityChecker { fn update(&mut self, msg: Self::Message) { match msg { - Msg::SetFunderActivityFetchState(fetch_state) => { - self.fetch_funder_activity.apply(fetch_state); - match self.fetch_funder_activity.as_ref().state() { + Msg::SetInstitutionActivityFetchState(fetch_state) => { + self.fetch_institution_activity.apply(fetch_state); + match self.fetch_institution_activity.as_ref().state() { FetchState::NotFetching(_) => (), FetchState::Fetching(_) => (), FetchState::Fetched(body) => { @@ -75,21 +75,21 @@ impl Agent for FunderActivityChecker { fn handle_input(&mut self, msg: Self::Input, _: HandlerId) { match msg { - Request::RetrieveFunderActivity(funder_id) => { - let body = FunderActivityRequestBody { + Request::RetrieveInstitutionActivity(institution_id) => { + let body = InstitutionActivityRequestBody { variables: Variables { - funder_id: Some(funder_id), + institution_id: Some(institution_id), }, ..Default::default() }; - let request = FunderActivityRequest { body }; - self.fetch_funder_activity = Fetch::new(request); + let request = InstitutionActivityRequest { body }; + self.fetch_institution_activity = Fetch::new(request); self.agent_link.send_future( - self.fetch_funder_activity - .fetch(Msg::SetFunderActivityFetchState), + self.fetch_institution_activity + .fetch(Msg::SetInstitutionActivityFetchState), ); self.agent_link - .send_message(Msg::SetFunderActivityFetchState(FetchAction::Fetching)); + .send_message(Msg::SetInstitutionActivityFetchState(FetchAction::Fetching)); } } } diff --git a/thoth-app/src/agent/mod.rs b/thoth-app/src/agent/mod.rs index ec3badd7..ba851ad6 100644 --- a/thoth-app/src/agent/mod.rs +++ b/thoth-app/src/agent/mod.rs @@ -63,7 +63,7 @@ macro_rules! timer_agent { } pub mod contributor_activity_checker; -pub mod funder_activity_checker; +pub mod institution_activity_checker; pub mod notification_bus; pub mod session_timer; pub mod version_timer; diff --git a/thoth-app/src/component/admin.rs b/thoth-app/src/component/admin.rs index 4db68450..fc68e54d 100644 --- a/thoth-app/src/component/admin.rs +++ b/thoth-app/src/component/admin.rs @@ -17,14 +17,14 @@ use crate::agent::notification_bus::Request; use crate::component::contributor::ContributorComponent; use crate::component::contributors::ContributorsComponent; use crate::component::dashboard::DashboardComponent; -use crate::component::funder::FunderComponent; -use crate::component::funders::FundersComponent; use crate::component::imprint::ImprintComponent; use crate::component::imprints::ImprintsComponent; +use crate::component::institution::InstitutionComponent; +use crate::component::institutions::InstitutionsComponent; use crate::component::menu::MenuComponent; use crate::component::new_contributor::NewContributorComponent; -use crate::component::new_funder::NewFunderComponent; use crate::component::new_imprint::NewImprintComponent; +use crate::component::new_institution::NewInstitutionComponent; use crate::component::new_publisher::NewPublisherComponent; use crate::component::new_series::NewSeriesComponent; use crate::component::new_work::NewWorkComponent; @@ -177,9 +177,9 @@ impl Component for AdminComponent { AdminRoute::Imprints => html!{}, AdminRoute::Imprint(id) => html!{}, AdminRoute::NewImprint => html!{}, - AdminRoute::Funders => html!{}, - AdminRoute::Funder(id) => html!{}, - AdminRoute::NewFunder => html!{}, + AdminRoute::Institutions => html!{}, + AdminRoute::Institution(id) => html!{}, + AdminRoute::NewInstitution => html!{}, AdminRoute::Publications => html!{}, AdminRoute::Publication(id) => html!{}, AdminRoute::NewPublication => { diff --git a/thoth-app/src/component/affiliations_form.rs b/thoth-app/src/component/affiliations_form.rs new file mode 100644 index 00000000..e069ab87 --- /dev/null +++ b/thoth-app/src/component/affiliations_form.rs @@ -0,0 +1,482 @@ +use thoth_api::model::affiliation::AffiliationWithInstitution; +use thoth_api::model::institution::Institution; +use uuid::Uuid; +use yew::html; +use yew::prelude::*; +use yew::ComponentLink; +use yewtil::fetch::Fetch; +use yewtil::fetch::FetchAction; +use yewtil::fetch::FetchState; +use yewtil::future::LinkFuture; +use yewtil::NeqAssign; + +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::FormNumberInput; +use crate::component::utils::FormTextInput; +use crate::models::affiliation::affiliations_query::AffiliationsRequest; +use crate::models::affiliation::affiliations_query::AffiliationsRequestBody; +use crate::models::affiliation::affiliations_query::FetchActionAffiliations; +use crate::models::affiliation::affiliations_query::FetchAffiliations; +use crate::models::affiliation::affiliations_query::Variables; +use crate::models::affiliation::create_affiliation_mutation::CreateAffiliationRequest; +use crate::models::affiliation::create_affiliation_mutation::CreateAffiliationRequestBody; +use crate::models::affiliation::create_affiliation_mutation::PushActionCreateAffiliation; +use crate::models::affiliation::create_affiliation_mutation::PushCreateAffiliation; +use crate::models::affiliation::create_affiliation_mutation::Variables as CreateVariables; +use crate::models::affiliation::delete_affiliation_mutation::DeleteAffiliationRequest; +use crate::models::affiliation::delete_affiliation_mutation::DeleteAffiliationRequestBody; +use crate::models::affiliation::delete_affiliation_mutation::PushActionDeleteAffiliation; +use crate::models::affiliation::delete_affiliation_mutation::PushDeleteAffiliation; +use crate::models::affiliation::delete_affiliation_mutation::Variables as DeleteVariables; +use crate::models::institution::institutions_query::FetchActionInstitutions; +use crate::models::institution::institutions_query::FetchInstitutions; +use crate::models::institution::institutions_query::InstitutionsRequest; +use crate::models::institution::institutions_query::InstitutionsRequestBody; +use crate::models::institution::institutions_query::Variables as SearchVariables; +use crate::models::Dropdown; +use crate::string::CANCEL_BUTTON; +use crate::string::REMOVE_BUTTON; + +use super::ToOption; + +pub struct AffiliationsFormComponent { + fetch_affiliations: FetchAffiliations, + props: Props, + data: AffiliationsFormData, + new_affiliation: AffiliationWithInstitution, + show_add_form: bool, + show_results: bool, + fetch_institutions: FetchInstitutions, + push_affiliation: PushCreateAffiliation, + delete_affiliation: PushDeleteAffiliation, + link: ComponentLink, + notification_bus: NotificationDispatcher, +} + +#[derive(Default)] +struct AffiliationsFormData { + institutions: Vec, + affiliations: Option>, +} + +pub enum Msg { + ToggleAddFormDisplay(bool), + SetAffiliationsFetchState(FetchActionAffiliations), + GetAffiliations, + SetInstitutionsFetchState(FetchActionInstitutions), + GetInstitutions, + ToggleSearchResultDisplay(bool), + SearchInstitution(String), + SetAffiliationPushState(PushActionCreateAffiliation), + CreateAffiliation, + SetAffiliationDeleteState(PushActionDeleteAffiliation), + DeleteAffiliation(Uuid), + AddAffiliation(Institution), + ChangePosition(String), + ChangeOrdinal(String), +} + +#[derive(Clone, Properties, PartialEq)] +pub struct Props { + pub contribution_id: Uuid, +} + +impl Component for AffiliationsFormComponent { + type Message = Msg; + type Properties = Props; + + fn create(props: Self::Properties, link: ComponentLink) -> Self { + let fetch_affiliations = Default::default(); + let data: AffiliationsFormData = Default::default(); + let new_affiliation: AffiliationWithInstitution = Default::default(); + let show_add_form = false; + let show_results = false; + let fetch_institutions = Default::default(); + let push_affiliation = Default::default(); + let delete_affiliation = Default::default(); + let notification_bus = NotificationBus::dispatcher(); + + link.send_message(Msg::GetAffiliations); + + AffiliationsFormComponent { + fetch_affiliations, + props, + data, + new_affiliation, + show_add_form, + show_results, + fetch_institutions, + push_affiliation, + delete_affiliation, + link, + notification_bus, + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::ToggleAddFormDisplay(value) => { + self.show_add_form = value; + true + } + Msg::SetAffiliationsFetchState(fetch_state) => { + self.fetch_affiliations.apply(fetch_state); + self.data.affiliations = match self.fetch_affiliations.as_ref().state() { + FetchState::NotFetching(_) => None, + FetchState::Fetching(_) => None, + FetchState::Fetched(body) => match &body.data.contribution { + Some(c) => c.affiliations.clone(), + None => Default::default(), + }, + FetchState::Failed(_, _err) => None, + }; + true + } + Msg::GetAffiliations => { + let body = AffiliationsRequestBody { + variables: Variables { + contribution_id: self.props.contribution_id, + }, + ..Default::default() + }; + let request = AffiliationsRequest { body }; + self.fetch_affiliations = Fetch::new(request); + + self.link.send_future( + self.fetch_affiliations + .fetch(Msg::SetAffiliationsFetchState), + ); + self.link + .send_message(Msg::SetAffiliationsFetchState(FetchAction::Fetching)); + false + } + Msg::SetInstitutionsFetchState(fetch_state) => { + self.fetch_institutions.apply(fetch_state); + self.data.institutions = match self.fetch_institutions.as_ref().state() { + FetchState::NotFetching(_) => vec![], + FetchState::Fetching(_) => vec![], + FetchState::Fetched(body) => body.data.institutions.clone(), + FetchState::Failed(_, _err) => vec![], + }; + true + } + Msg::GetInstitutions => { + self.link.send_future( + self.fetch_institutions + .fetch(Msg::SetInstitutionsFetchState), + ); + self.link + .send_message(Msg::SetInstitutionsFetchState(FetchAction::Fetching)); + false + } + Msg::SetAffiliationPushState(fetch_state) => { + self.push_affiliation.apply(fetch_state); + match self.push_affiliation.clone().state() { + FetchState::NotFetching(_) => false, + FetchState::Fetching(_) => false, + FetchState::Fetched(body) => match &body.data.create_affiliation { + Some(i) => { + let affiliation = i.clone(); + let mut affiliations: Vec = + self.data.affiliations.clone().unwrap_or_default(); + affiliations.push(affiliation); + self.data.affiliations = Some(affiliations); + self.link.send_message(Msg::ToggleAddFormDisplay(false)); + true + } + None => { + self.link.send_message(Msg::ToggleAddFormDisplay(false)); + self.notification_bus.send(Request::NotificationBusMsg(( + "Failed to save".to_string(), + NotificationStatus::Danger, + ))); + false + } + }, + FetchState::Failed(_, err) => { + self.link.send_message(Msg::ToggleAddFormDisplay(false)); + self.notification_bus.send(Request::NotificationBusMsg(( + err.to_string(), + NotificationStatus::Danger, + ))); + false + } + } + } + Msg::CreateAffiliation => { + let body = CreateAffiliationRequestBody { + variables: CreateVariables { + contribution_id: self.props.contribution_id, + institution_id: self.new_affiliation.institution_id, + position: self.new_affiliation.position.clone(), + affiliation_ordinal: self.new_affiliation.affiliation_ordinal, + }, + ..Default::default() + }; + let request = CreateAffiliationRequest { body }; + self.push_affiliation = Fetch::new(request); + self.link + .send_future(self.push_affiliation.fetch(Msg::SetAffiliationPushState)); + self.link + .send_message(Msg::SetAffiliationPushState(FetchAction::Fetching)); + false + } + Msg::SetAffiliationDeleteState(fetch_state) => { + self.delete_affiliation.apply(fetch_state); + match self.delete_affiliation.clone().state() { + FetchState::NotFetching(_) => false, + FetchState::Fetching(_) => false, + FetchState::Fetched(body) => match &body.data.delete_affiliation { + Some(affiliation) => { + let to_keep: Vec = self + .data + .affiliations + .clone() + .unwrap_or_default() + .into_iter() + .filter(|a| a.affiliation_id != affiliation.affiliation_id) + .collect(); + self.data.affiliations = Some(to_keep); + true + } + None => { + self.notification_bus.send(Request::NotificationBusMsg(( + "Failed to save".to_string(), + NotificationStatus::Danger, + ))); + false + } + }, + FetchState::Failed(_, err) => { + self.notification_bus.send(Request::NotificationBusMsg(( + err.to_string(), + NotificationStatus::Danger, + ))); + false + } + } + } + Msg::DeleteAffiliation(affiliation_id) => { + let body = DeleteAffiliationRequestBody { + variables: DeleteVariables { affiliation_id }, + ..Default::default() + }; + let request = DeleteAffiliationRequest { body }; + self.delete_affiliation = Fetch::new(request); + self.link.send_future( + self.delete_affiliation + .fetch(Msg::SetAffiliationDeleteState), + ); + self.link + .send_message(Msg::SetAffiliationDeleteState(FetchAction::Fetching)); + false + } + Msg::AddAffiliation(institution) => { + self.new_affiliation.institution_id = institution.institution_id; + self.new_affiliation.institution = institution; + self.link.send_message(Msg::ToggleAddFormDisplay(true)); + true + } + Msg::ToggleSearchResultDisplay(value) => { + self.show_results = value; + true + } + Msg::SearchInstitution(value) => { + let body = InstitutionsRequestBody { + variables: SearchVariables { + filter: Some(value), + limit: Some(9999), + ..Default::default() + }, + ..Default::default() + }; + let request = InstitutionsRequest { body }; + self.fetch_institutions = Fetch::new(request); + self.link.send_message(Msg::GetInstitutions); + false + } + Msg::ChangePosition(val) => self + .new_affiliation + .position + .neq_assign(val.to_opt_string()), + Msg::ChangeOrdinal(ordinal) => { + let ordinal = ordinal.parse::().unwrap_or(0); + self.new_affiliation.affiliation_ordinal.neq_assign(ordinal); + false // otherwise we re-render the component and reset the value + } + } + } + + fn change(&mut self, props: Self::Properties) -> ShouldRender { + if self.props.neq_assign(props) { + self.link.send_message(Msg::GetAffiliations); + true + } else { + false + } + } + + fn view(&self) -> Html { + // Ensure the form has a unique ID, as there may be multiple copies of + // the form on the same parent page, and ID clashes can lead to bugs + let form_id = format!("affiliations-form-{}", self.props.contribution_id); + let affiliations = self.data.affiliations.clone().unwrap_or_default(); + let close_modal = self.link.callback(|e: MouseEvent| { + e.prevent_default(); + Msg::ToggleAddFormDisplay(false) + }); + html! { +
+
+ + +
+ + + + + + + // Empty column for "Remove" buttons + + + + + {for affiliations.iter().map(|a| self.render_affiliation(a))} + +
+ + +
+ + +
+ { "Institution" } + + { "Position" } + + { "Affiliation Ordinal" } +
+
+ } + } +} + +impl AffiliationsFormComponent { + fn add_form_status(&self) -> String { + match self.show_add_form { + true => "modal is-active".to_string(), + false => "modal".to_string(), + } + } + + fn search_dropdown_status(&self) -> String { + match self.show_results { + true => "dropdown is-active".to_string(), + false => "dropdown".to_string(), + } + } + + fn render_affiliation(&self, a: &AffiliationWithInstitution) -> Html { + let affiliation_id = a.affiliation_id; + html! { + + {&a.institution.institution_name} + {&a.position.clone().unwrap_or_else(|| "".to_string())} + {&a.affiliation_ordinal.clone()} + + + { REMOVE_BUTTON } + + + + } + } +} diff --git a/thoth-app/src/component/contributions_form.rs b/thoth-app/src/component/contributions_form.rs index a446647d..4153b85d 100644 --- a/thoth-app/src/component/contributions_form.rs +++ b/thoth-app/src/component/contributions_form.rs @@ -16,6 +16,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::affiliations_form::AffiliationsFormComponent; use crate::component::utils::FormBooleanSelect; use crate::component::utils::FormContributionTypeSelect; use crate::component::utils::FormNumberInput; @@ -83,7 +84,6 @@ pub enum Msg { ChangeFirstName(String), ChangeLastName(String), ChangeFullName(String), - ChangeInstitution(String), ChangeBiography(String), ChangeContributiontype(ContributionType), ChangeMainContribution(bool), @@ -217,7 +217,6 @@ impl Component for ContributionsFormComponent { contribution_type: self.new_contribution.contribution_type, main_contribution: self.new_contribution.main_contribution, biography: self.new_contribution.biography.clone(), - institution: self.new_contribution.institution.clone(), first_name: self.new_contribution.first_name.clone(), last_name: self.new_contribution.last_name.clone(), full_name: self.new_contribution.full_name.clone(), @@ -246,10 +245,7 @@ impl Component for ContributionsFormComponent { .clone() .unwrap_or_default() .into_iter() - .filter(|c| { - c.contributor_id != contribution.contributor_id - || c.contribution_type != contribution.contribution_type - }) + .filter(|c| c.contribution_id != contribution.contribution_id) .collect(); self.props.update_contributions.emit(Some(to_keep)); true @@ -324,10 +320,6 @@ impl Component for ContributionsFormComponent { .new_contribution .full_name .neq_assign(val.trim().to_owned()), - Msg::ChangeInstitution(val) => self - .new_contribution - .institution - .neq_assign(val.to_opt_string()), Msg::ChangeBiography(val) => self .new_contribution .biography @@ -447,11 +439,6 @@ impl Component for ContributionsFormComponent { data=self.data.contribution_types.clone() required = true /> - Html { let contribution_id = c.contribution_id; html! { -
+
@@ -547,12 +534,6 @@ impl ContributionsFormComponent { {&c.contribution_type}
-
- -
- {&c.institution.clone().unwrap_or_else(|| "".to_string())} -
-
@@ -589,6 +570,9 @@ impl ContributionsFormComponent {
+ } } diff --git a/thoth-app/src/component/funder.rs b/thoth-app/src/component/funder.rs deleted file mode 100644 index e0061ca2..00000000 --- a/thoth-app/src/component/funder.rs +++ /dev/null @@ -1,399 +0,0 @@ -use thoth_api::model::funder::Funder; -use thoth_api::model::funding::FundingWithWork; -use thoth_api::model::{Doi, DOI_DOMAIN}; -use thoth_errors::ThothError; -use uuid::Uuid; -use yew::html; -use yew::prelude::*; -use yew::ComponentLink; -use yew_router::agent::RouteAgentDispatcher; -use yew_router::agent::RouteRequest; -use yew_router::prelude::RouterAnchor; -use yew_router::route::Route; -use yewtil::fetch::Fetch; -use yewtil::fetch::FetchAction; -use yewtil::fetch::FetchState; -use yewtil::future::LinkFuture; -use yewtil::NeqAssign; - -use crate::agent::funder_activity_checker::FunderActivityChecker; -use crate::agent::funder_activity_checker::Request as FunderActivityRequest; -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::utils::FormTextInput; -use crate::component::utils::FormTextInputExtended; -use crate::component::utils::Loader; -use crate::models::funder::delete_funder_mutation::DeleteFunderRequest; -use crate::models::funder::delete_funder_mutation::DeleteFunderRequestBody; -use crate::models::funder::delete_funder_mutation::PushActionDeleteFunder; -use crate::models::funder::delete_funder_mutation::PushDeleteFunder; -use crate::models::funder::delete_funder_mutation::Variables as DeleteVariables; -use crate::models::funder::funder_activity_query::FunderActivityResponseData; -use crate::models::funder::funder_query::FetchActionFunder; -use crate::models::funder::funder_query::FetchFunder; -use crate::models::funder::funder_query::FunderRequest; -use crate::models::funder::funder_query::FunderRequestBody; -use crate::models::funder::funder_query::Variables; -use crate::models::funder::update_funder_mutation::PushActionUpdateFunder; -use crate::models::funder::update_funder_mutation::PushUpdateFunder; -use crate::models::funder::update_funder_mutation::UpdateFunderRequest; -use crate::models::funder::update_funder_mutation::UpdateFunderRequestBody; -use crate::models::funder::update_funder_mutation::Variables as UpdateVariables; -use crate::models::EditRoute; -use crate::route::AdminRoute; -use crate::route::AppRoute; -use crate::string::SAVE_BUTTON; - -pub struct FunderComponent { - funder: Funder, - // Track the user-entered DOI string, which may not be validly formatted - funder_doi: String, - funder_doi_warning: String, - fetch_funder: FetchFunder, - push_funder: PushUpdateFunder, - delete_funder: PushDeleteFunder, - link: ComponentLink, - router: RouteAgentDispatcher<()>, - notification_bus: NotificationDispatcher, - _funder_activity_checker: Box>, - funder_activity: Vec, -} - -pub enum Msg { - GetFunderActivity(FunderActivityResponseData), - SetFunderFetchState(FetchActionFunder), - GetFunder, - SetFunderPushState(PushActionUpdateFunder), - UpdateFunder, - SetFunderDeleteState(PushActionDeleteFunder), - DeleteFunder, - ChangeFunderName(String), - ChangeFunderDoi(String), - ChangeRoute(AppRoute), -} - -#[derive(Clone, Properties)] -pub struct Props { - pub funder_id: Uuid, -} - -impl Component for FunderComponent { - type Message = Msg; - type Properties = Props; - - fn create(props: Self::Properties, link: ComponentLink) -> Self { - let body = FunderRequestBody { - variables: Variables { - funder_id: Some(props.funder_id), - }, - ..Default::default() - }; - let request = FunderRequest { body }; - let fetch_funder = Fetch::new(request); - let push_funder = Default::default(); - let delete_funder = Default::default(); - let notification_bus = NotificationBus::dispatcher(); - let funder: Funder = Default::default(); - let funder_doi = Default::default(); - let funder_doi_warning = Default::default(); - let router = RouteAgentDispatcher::new(); - let mut _funder_activity_checker = - FunderActivityChecker::bridge(link.callback(Msg::GetFunderActivity)); - let funder_activity = Default::default(); - - link.send_message(Msg::GetFunder); - _funder_activity_checker.send(FunderActivityRequest::RetrieveFunderActivity( - props.funder_id, - )); - - FunderComponent { - funder, - funder_doi, - funder_doi_warning, - fetch_funder, - push_funder, - delete_funder, - link, - router, - notification_bus, - _funder_activity_checker, - funder_activity, - } - } - - fn update(&mut self, msg: Self::Message) -> ShouldRender { - match msg { - Msg::GetFunderActivity(response) => { - let mut should_render = false; - if let Some(funder) = response.funder { - if let Some(fundings) = funder.fundings { - if !fundings.is_empty() { - self.funder_activity = fundings; - should_render = true; - } - } - } - should_render - } - Msg::SetFunderFetchState(fetch_state) => { - self.fetch_funder.apply(fetch_state); - match self.fetch_funder.as_ref().state() { - FetchState::NotFetching(_) => false, - FetchState::Fetching(_) => false, - FetchState::Fetched(body) => { - self.funder = match &body.data.funder { - Some(c) => c.to_owned(), - None => Default::default(), - }; - // Initialise user-entered DOI variable to match DOI in database - self.funder_doi = self - .funder - .funder_doi - .clone() - .unwrap_or_default() - .to_string(); - true - } - FetchState::Failed(_, _err) => false, - } - } - Msg::GetFunder => { - self.link - .send_future(self.fetch_funder.fetch(Msg::SetFunderFetchState)); - self.link - .send_message(Msg::SetFunderFetchState(FetchAction::Fetching)); - false - } - Msg::SetFunderPushState(fetch_state) => { - self.push_funder.apply(fetch_state); - match self.push_funder.as_ref().state() { - FetchState::NotFetching(_) => false, - FetchState::Fetching(_) => false, - FetchState::Fetched(body) => match &body.data.update_funder { - Some(f) => { - // Save was successful: update user-entered DOI variable to match DOI in database - self.funder_doi = self - .funder - .funder_doi - .clone() - .unwrap_or_default() - .to_string(); - self.funder_doi_warning.clear(); - self.notification_bus.send(Request::NotificationBusMsg(( - format!("Saved {}", f.funder_name), - NotificationStatus::Success, - ))); - true - } - None => { - self.notification_bus.send(Request::NotificationBusMsg(( - "Failed to save".to_string(), - NotificationStatus::Danger, - ))); - false - } - }, - FetchState::Failed(_, err) => { - self.notification_bus.send(Request::NotificationBusMsg(( - err.to_string(), - NotificationStatus::Danger, - ))); - false - } - } - } - Msg::UpdateFunder => { - // Only update the DOI value with the current user-entered string - // if it is validly formatted - otherwise keep the database version. - // If no DOI was provided, no format check is required. - if self.funder_doi.is_empty() { - self.funder.funder_doi.neq_assign(None); - } else if let Ok(result) = self.funder_doi.parse::() { - self.funder.funder_doi.neq_assign(Some(result)); - } - let body = UpdateFunderRequestBody { - variables: UpdateVariables { - funder_id: self.funder.funder_id, - funder_name: self.funder.funder_name.clone(), - funder_doi: self.funder.funder_doi.clone(), - }, - ..Default::default() - }; - let request = UpdateFunderRequest { body }; - self.push_funder = Fetch::new(request); - self.link - .send_future(self.push_funder.fetch(Msg::SetFunderPushState)); - self.link - .send_message(Msg::SetFunderPushState(FetchAction::Fetching)); - false - } - Msg::SetFunderDeleteState(fetch_state) => { - self.delete_funder.apply(fetch_state); - match self.delete_funder.as_ref().state() { - FetchState::NotFetching(_) => false, - FetchState::Fetching(_) => false, - FetchState::Fetched(body) => match &body.data.delete_funder { - Some(f) => { - self.notification_bus.send(Request::NotificationBusMsg(( - format!("Deleted {}", f.funder_name), - NotificationStatus::Success, - ))); - self.link.send_message(Msg::ChangeRoute(AppRoute::Admin( - AdminRoute::Funders, - ))); - true - } - None => { - self.notification_bus.send(Request::NotificationBusMsg(( - "Failed to save".to_string(), - NotificationStatus::Danger, - ))); - false - } - }, - FetchState::Failed(_, err) => { - self.notification_bus.send(Request::NotificationBusMsg(( - err.to_string(), - NotificationStatus::Danger, - ))); - false - } - } - } - Msg::DeleteFunder => { - let body = DeleteFunderRequestBody { - variables: DeleteVariables { - funder_id: self.funder.funder_id, - }, - ..Default::default() - }; - let request = DeleteFunderRequest { body }; - self.delete_funder = Fetch::new(request); - self.link - .send_future(self.delete_funder.fetch(Msg::SetFunderDeleteState)); - self.link - .send_message(Msg::SetFunderDeleteState(FetchAction::Fetching)); - false - } - Msg::ChangeFunderName(funder_name) => self - .funder - .funder_name - .neq_assign(funder_name.trim().to_owned()), - Msg::ChangeFunderDoi(value) => { - if self.funder_doi.neq_assign(value.trim().to_owned()) { - // If DOI is not correctly formatted, display a warning. - // Don't update self.funder.funder_doi yet, as user may later - // overwrite a new valid value with an invalid one. - self.funder_doi_warning.clear(); - match self.funder_doi.parse::() { - Err(e) => { - match e { - // If no DOI was provided, no warning is required. - ThothError::DoiEmptyError => {} - _ => self.funder_doi_warning = e.to_string(), - } - } - Ok(value) => self.funder_doi = value.to_string(), - } - true - } else { - false - } - } - Msg::ChangeRoute(r) => { - let route = Route::from(r); - self.router.send(RouteRequest::ChangeRoute(route)); - false - } - } - } - - fn change(&mut self, _props: Self::Properties) -> ShouldRender { - false - } - - fn view(&self) -> Html { - match self.fetch_funder.as_ref().state() { - FetchState::NotFetching(_) => html! {}, - FetchState::Fetching(_) => html! {}, - FetchState::Fetched(_body) => { - let callback = self.link.callback(|event: FocusEvent| { - event.prevent_default(); - Msg::UpdateFunder - }); - html! { - <> - - - { if !self.funder_activity.is_empty() { - html! { - - } - } else { - html! {} - } - } - -
- - - -
-
- -
-
- - - } - } - FetchState::Failed(_, err) => html! {&err}, - } - } -} diff --git a/thoth-app/src/component/funders.rs b/thoth-app/src/component/funders.rs deleted file mode 100644 index 649546ae..00000000 --- a/thoth-app/src/component/funders.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::models::funder::funders_query::FetchActionFunders; -use crate::models::funder::funders_query::FetchFunders; -use crate::models::funder::funders_query::FundersRequest; -use crate::models::funder::funders_query::FundersRequestBody; -use crate::models::funder::funders_query::Variables; -use thoth_api::model::funder::Funder; -use thoth_api::model::funder::FunderField; -use thoth_api::model::funder::FunderOrderBy; - -pagination_component! { - FundersComponent, - Funder, - funders, - funder_count, - FundersRequest, - FetchActionFunders, - FetchFunders, - FundersRequestBody, - Variables, - SEARCH_FUNDERS, - PAGINATION_COUNT_FUNDERS, - vec![ - FunderField::FunderId.to_string(), - FunderField::FunderName.to_string(), - FunderField::FunderDoi.to_string(), - FunderField::UpdatedAt.to_string(), - ], - FunderOrderBy, - FunderField, -} diff --git a/thoth-app/src/component/fundings_form.rs b/thoth-app/src/component/fundings_form.rs index a5dea4f4..0329e108 100644 --- a/thoth-app/src/component/fundings_form.rs +++ b/thoth-app/src/component/fundings_form.rs @@ -1,5 +1,5 @@ -use thoth_api::model::funder::Funder; -use thoth_api::model::funding::FundingWithFunder; +use thoth_api::model::funding::FundingWithInstitution; +use thoth_api::model::institution::Institution; use uuid::Uuid; use yew::html; use yew::prelude::*; @@ -15,11 +15,6 @@ use crate::agent::notification_bus::NotificationDispatcher; use crate::agent::notification_bus::NotificationStatus; use crate::agent::notification_bus::Request; use crate::component::utils::FormTextInput; -use crate::models::funder::funders_query::FetchActionFunders; -use crate::models::funder::funders_query::FetchFunders; -use crate::models::funder::funders_query::FundersRequest; -use crate::models::funder::funders_query::FundersRequestBody; -use crate::models::funder::funders_query::Variables; use crate::models::funding::create_funding_mutation::CreateFundingRequest; use crate::models::funding::create_funding_mutation::CreateFundingRequestBody; use crate::models::funding::create_funding_mutation::PushActionCreateFunding; @@ -30,6 +25,11 @@ use crate::models::funding::delete_funding_mutation::DeleteFundingRequestBody; use crate::models::funding::delete_funding_mutation::PushActionDeleteFunding; use crate::models::funding::delete_funding_mutation::PushDeleteFunding; use crate::models::funding::delete_funding_mutation::Variables as DeleteVariables; +use crate::models::institution::institutions_query::FetchActionInstitutions; +use crate::models::institution::institutions_query::FetchInstitutions; +use crate::models::institution::institutions_query::InstitutionsRequest; +use crate::models::institution::institutions_query::InstitutionsRequestBody; +use crate::models::institution::institutions_query::Variables; use crate::models::Dropdown; use crate::string::CANCEL_BUTTON; use crate::string::EMPTY_FUNDINGS; @@ -40,10 +40,10 @@ use super::ToOption; pub struct FundingsFormComponent { props: Props, data: FundingsFormData, - new_funding: FundingWithFunder, + new_funding: FundingWithInstitution, show_add_form: bool, show_results: bool, - fetch_funders: FetchFunders, + fetch_institutions: FetchInstitutions, push_funding: PushCreateFunding, delete_funding: PushDeleteFunding, link: ComponentLink, @@ -52,20 +52,21 @@ pub struct FundingsFormComponent { #[derive(Default)] struct FundingsFormData { - funders: Vec, + institutions: Vec, } +#[allow(clippy::large_enum_variant)] pub enum Msg { ToggleAddFormDisplay(bool), - SetFundersFetchState(FetchActionFunders), - GetFunders, + SetInstitutionsFetchState(FetchActionInstitutions), + GetInstitutions, ToggleSearchResultDisplay(bool), - SearchFunder(String), + SearchInstitution(String), SetFundingPushState(PushActionCreateFunding), CreateFunding, SetFundingDeleteState(PushActionDeleteFunding), DeleteFunding(Uuid), - AddFunding(Funder), + AddFunding(Institution), ChangeProgram(String), ChangeProjectName(String), ChangeProjectShortname(String), @@ -75,9 +76,9 @@ pub enum Msg { #[derive(Clone, Properties, PartialEq)] pub struct Props { - pub fundings: Option>, + pub fundings: Option>, pub work_id: Uuid, - pub update_fundings: Callback>>, + pub update_fundings: Callback>>, } impl Component for FundingsFormComponent { @@ -86,15 +87,15 @@ impl Component for FundingsFormComponent { fn create(props: Self::Properties, link: ComponentLink) -> Self { let data: FundingsFormData = Default::default(); - let new_funding: FundingWithFunder = Default::default(); + let new_funding: FundingWithInstitution = Default::default(); let show_add_form = false; let show_results = false; - let fetch_funders = Default::default(); + let fetch_institutions = Default::default(); let push_funding = Default::default(); let delete_funding = Default::default(); let notification_bus = NotificationBus::dispatcher(); - link.send_message(Msg::GetFunders); + link.send_message(Msg::GetInstitutions); FundingsFormComponent { props, @@ -102,7 +103,7 @@ impl Component for FundingsFormComponent { new_funding, show_add_form, show_results, - fetch_funders, + fetch_institutions, push_funding, delete_funding, link, @@ -116,21 +117,23 @@ impl Component for FundingsFormComponent { self.show_add_form = value; true } - Msg::SetFundersFetchState(fetch_state) => { - self.fetch_funders.apply(fetch_state); - self.data.funders = match self.fetch_funders.clone().state() { + Msg::SetInstitutionsFetchState(fetch_state) => { + self.fetch_institutions.apply(fetch_state); + self.data.institutions = match self.fetch_institutions.clone().state() { FetchState::NotFetching(_) => vec![], FetchState::Fetching(_) => vec![], - FetchState::Fetched(body) => body.data.funders, + FetchState::Fetched(body) => body.data.institutions, FetchState::Failed(_, _err) => vec![], }; true } - Msg::GetFunders => { + Msg::GetInstitutions => { + self.link.send_future( + self.fetch_institutions + .fetch(Msg::SetInstitutionsFetchState), + ); self.link - .send_future(self.fetch_funders.fetch(Msg::SetFundersFetchState)); - self.link - .send_message(Msg::SetFundersFetchState(FetchAction::Fetching)); + .send_message(Msg::SetInstitutionsFetchState(FetchAction::Fetching)); false } Msg::SetFundingPushState(fetch_state) => { @@ -141,7 +144,7 @@ impl Component for FundingsFormComponent { FetchState::Fetched(body) => match &body.data.create_funding { Some(i) => { let funding = i.clone(); - let mut fundings: Vec = + let mut fundings: Vec = self.props.fundings.clone().unwrap_or_default(); fundings.push(funding); self.props.update_fundings.emit(Some(fundings)); @@ -171,7 +174,7 @@ impl Component for FundingsFormComponent { let body = CreateFundingRequestBody { variables: CreateVariables { work_id: self.props.work_id, - funder_id: self.new_funding.funder_id, + institution_id: self.new_funding.institution_id, program: self.new_funding.program.clone(), project_name: self.new_funding.project_name.clone(), project_shortname: self.new_funding.project_shortname.clone(), @@ -195,7 +198,7 @@ impl Component for FundingsFormComponent { FetchState::Fetching(_) => false, FetchState::Fetched(body) => match &body.data.delete_funding { Some(funding) => { - let to_keep: Vec = self + let to_keep: Vec = self .props .fundings .clone() @@ -236,9 +239,9 @@ impl Component for FundingsFormComponent { .send_message(Msg::SetFundingDeleteState(FetchAction::Fetching)); false } - Msg::AddFunding(funder) => { - self.new_funding.funder_id = funder.funder_id; - self.new_funding.funder = funder; + Msg::AddFunding(institution) => { + self.new_funding.institution_id = institution.institution_id; + self.new_funding.institution = institution; self.link.send_message(Msg::ToggleAddFormDisplay(true)); true } @@ -246,8 +249,8 @@ impl Component for FundingsFormComponent { self.show_results = value; true } - Msg::SearchFunder(value) => { - let body = FundersRequestBody { + Msg::SearchInstitution(value) => { + let body = InstitutionsRequestBody { variables: Variables { filter: Some(value), limit: Some(9999), @@ -255,9 +258,9 @@ impl Component for FundingsFormComponent { }, ..Default::default() }; - let request = FundersRequest { body }; - self.fetch_funders = Fetch::new(request); - self.link.send_message(Msg::GetFunders); + let request = InstitutionsRequest { body }; + self.fetch_institutions = Fetch::new(request); + self.link.send_message(Msg::GetInstitutions); false } Msg::ChangeProgram(val) => self.new_funding.program.neq_assign(val.to_opt_string()), @@ -303,10 +306,10 @@ impl Component for FundingsFormComponent { @@ -316,14 +319,14 @@ impl Component for FundingsFormComponent {

-