From 960fbbf598e3ad87c0b5b35ed12f425f09d4dada Mon Sep 17 00:00:00 2001 From: steffengreiner Date: Wed, 17 Mar 2021 13:31:00 +0100 Subject: [PATCH 01/32] Bump version in Changelog and pom and qube.cfg --- CHANGELOG.rst | 11 +++++++++++ offer-manager-app/pom.xml | 4 ++-- offer-manager-domain/pom.xml | 2 +- pom.xml | 2 +- qube.cfg | 2 +- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6a30e91e6..d70e08526 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,17 @@ Changelog This project adheres to `Semantic Versioning `_. +1.0.0-alpha.5-SNAPSHOT +-------------------------- + +**Added** + +**Fixed** + +**Dependencies** + +**Deprecated** + 1.0.0-alpha.4 (2021-03-16) -------------------------- diff --git a/offer-manager-app/pom.xml b/offer-manager-app/pom.xml index b02cd3871..29d71ce3a 100644 --- a/offer-manager-app/pom.xml +++ b/offer-manager-app/pom.xml @@ -5,7 +5,7 @@ offer-manager life.qbic - 1.0.0-alpha.4 + 1.0.0-alpha.5-SNAPSHOT 4.0.0 war @@ -15,7 +15,7 @@ life.qbic offer-manager-domain - 1.0.0-alpha.4 + 1.0.0-alpha.5-SNAPSHOT compile diff --git a/offer-manager-domain/pom.xml b/offer-manager-domain/pom.xml index 121a14f41..e06794614 100644 --- a/offer-manager-domain/pom.xml +++ b/offer-manager-domain/pom.xml @@ -7,7 +7,7 @@ offer-manager life.qbic - 1.0.0-alpha.4 + 1.0.0-alpha.5-SNAPSHOT diff --git a/pom.xml b/pom.xml index e2fa503a9..6ed6d6013 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ offer-manager-app offer-manager - 1.0.0-alpha.4 + 1.0.0-alpha.5-SNAPSHOT life.qbic The new offer manager http://github.com/qbicsoftware/qOffer_2.0 diff --git a/qube.cfg b/qube.cfg index 11a3da7f3..a831f5b62 100644 --- a/qube.cfg +++ b/qube.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.0-SNAPSHOT +current_version = 1.0.0-alpha.5-SNAPSHOT [bumpversion_files_whitelisted] dot_qube = .qube.yml From c41105c09f97f5296717d83f49e977c05dfd8784 Mon Sep 17 00:00:00 2001 From: steffengreiner Date: Wed, 17 Mar 2021 15:10:49 +0100 Subject: [PATCH 02/32] Add date to changelog --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d70e08526..080159711 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,7 @@ Changelog This project adheres to `Semantic Versioning `_. -1.0.0-alpha.5-SNAPSHOT +1.0.0-alpha.5-SNAPSHOT (2021-03-17) -------------------------- **Added** From b9b018b01372d06352531ccc9ef614a055a03cf1 Mon Sep 17 00:00:00 2001 From: Sven F Date: Thu, 18 Mar 2021 10:04:46 +0100 Subject: [PATCH 03/32] Link project to offer (#410) This PR extends the OfferOverview DTO and Offer business object to support the association with a project. --- CHANGELOG.rst | 4 +++ .../dataresources/offers/OfferOverview.groovy | 25 +++++++++++++++++++ .../qbic/business/offers/Converter.groovy | 19 +++++++++++--- .../life/qbic/business/offers/Offer.groovy | 23 ++++++++++++++++- pom.xml | 2 +- 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 080159711..7e3acf813 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,10 @@ This project adheres to `Semantic Versioning `_. **Added** +* Link offers to project now. The ``life.qbic.business.offers.Offer`` and ``life.qbic.portal +.offermanager.dataresources.offers`` have been extended with a new property to associate it with +an existing project by its project identifier. + **Fixed** **Dependencies** diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferOverview.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferOverview.groovy index 98ca7aa27..5864f4047 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferOverview.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferOverview.groovy @@ -1,6 +1,7 @@ package life.qbic.portal.offermanager.dataresources.offers import life.qbic.datamodel.dtos.business.OfferId +import life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier /** * This class holds data for an offer overview @@ -15,6 +16,10 @@ class OfferOverview { final String projectTitle + /** + * @deprecated Use the {@link #associatedProject} property to link an offer with a project + */ + @Deprecated final String projectId final String customer @@ -25,6 +30,9 @@ class OfferOverview { final OfferId offerId + final Optional associatedProject + + @Deprecated OfferOverview( OfferId offerId, Date modificationDate, @@ -38,5 +46,22 @@ class OfferOverview { this.projectTitle = projectTitle this.customer = customer this.totalPrice = totalPrice + this.associatedProject = Optional.empty() + } + + OfferOverview( + OfferId offerId, + Date modificationDate, + String projectTitle, + String customer, + double totalPrice, + ProjectIdentifier associatedProject) { + this.offerId = offerId + this.modificationDate = modificationDate + this.projectId = "" + this.projectTitle = projectTitle + this.customer = customer + this.totalPrice = totalPrice + this.associatedProject = Optional.of(associatedProject) } } diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Converter.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Converter.groovy index 3e0a29793..2d760d918 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Converter.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Converter.groovy @@ -24,7 +24,7 @@ import life.qbic.datamodel.dtos.business.ProjectManager */ class Converter { static life.qbic.datamodel.dtos.business.Offer convertOfferToDTO(Offer offer) { - new life.qbic.datamodel.dtos.business.Offer.Builder( + def builder = new life.qbic.datamodel.dtos.business.Offer.Builder( offer.customer, offer.projectManager, offer.projectTitle, @@ -44,7 +44,12 @@ class Converter { .itemsWithOverheadNet(offer.overheadItemsNet) .itemsWithoutOverheadNet(offer.noOverheadItemsNet) .overheadRatio(offer.overheadRatio) - .build() + // Add the project identifier, if one is present + if (offer.associatedProject.isPresent()) { + builder.associatedProject(offer.associatedProject.get()) + } + + return builder.build() } static Offer buildOfferForCostCalculation(List items, Affiliation affiliation) { @@ -76,7 +81,7 @@ class Converter { } static life.qbic.business.offers.Offer convertDTOToOffer(life.qbic.datamodel.dtos.business.Offer offer) { - new Offer.Builder( + def builder = new Offer.Builder( offer.customer, offer.projectManager, offer.projectTitle, @@ -86,6 +91,12 @@ class Converter { .identifier(buildOfferId(offer.identifier)) //ToDo Is this the correct mapping? .creationDate(offer.modificationDate) - .build() + + // We optionally add the associated project, if present + if (offer.associatedProject.isPresent()) { + builder.associatedProject(offer.associatedProject.get()) + } + + return builder.build() } } diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy index 5686fc735..5ea816498 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy @@ -15,6 +15,7 @@ import life.qbic.datamodel.dtos.business.services.ProjectManagement import life.qbic.business.offers.identifier.OfferId import life.qbic.datamodel.dtos.business.services.SecondaryAnalysis import life.qbic.datamodel.dtos.business.services.Sequencing +import life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier import java.nio.charset.StandardCharsets import java.security.MessageDigest @@ -105,6 +106,11 @@ class Offer { */ private static final double VAT = 0.19 + /** + * A project that has been created from this offer (optional) + */ + private Optional associatedProject + private static Date calculateExpirationDate(Date date) { use (TimeCategory) { return date + 90.days @@ -123,6 +129,7 @@ class Offer { Affiliation selectedCustomerAffiliation List availableVersions double overheadRatio + Optional associatedProject Builder(Customer customer, ProjectManager projectManager, String projectTitle, String projectObjective, List items, Affiliation selectedCustomerAffiliation) { this.customer = Objects.requireNonNull(customer, "Customer must not be null") @@ -136,6 +143,7 @@ class Offer { // copy all immutable items to out internal list items.each {this.items.add(it)} this.selectedCustomerAffiliation = Objects.requireNonNull(selectedCustomerAffiliation, "Customer Affiliation must not be null") + this.associatedProject = Optional.empty() } Builder creationDate(Date creationDate) { @@ -158,6 +166,11 @@ class Offer { return this } + Builder associatedProject(ProjectIdentifier associatedProject) { + this.associatedProject = Optional.of(associatedProject) + return this + } + Offer build() { return new Offer(this) @@ -185,7 +198,11 @@ class Offer { this.itemsWithOverheadNetPrice = getOverheadItemsNet() this.itemsWithoutOverheadNetPrice = getNoOverheadItemsNet() this.overheadRatio = determineOverhead() - + if (builder.associatedProject.isPresent()) { + this.associatedProject = Optional.of(builder.associatedProject.get()) + } else { + this.associatedProject = Optional.empty() + } } /** @@ -343,6 +360,10 @@ class Offer { return overheadRatio } + Optional getAssociatedProject() { + return associatedProject + } + /** * Returns a deep copy of all available offer versions. * diff --git a/pom.xml b/pom.xml index 6ed6d6013..4916c1686 100644 --- a/pom.xml +++ b/pom.xml @@ -119,7 +119,7 @@ life.qbic data-model-lib - 2.3.0 + 2.4.0-SNAPSHOT com.vaadin From 725cef27d2a1c069db57be4d08ccf7924b48abce Mon Sep 17 00:00:00 2001 From: jnnfr Date: Thu, 18 Mar 2021 16:23:43 +0100 Subject: [PATCH 04/32] Feature/add product converter (#412) Introduce and apply ProductCategory converter Co-authored-by: Sven F. --- .../offer/create/SelectItemsView.groovy | 20 ----- .../MaintainProductsController.groovy | 2 +- .../MaintainProductsPresenter.groovy | 2 +- .../products/MaintainProductsView.groovy | 24 ------ .../products/MaintainProductsViewModel.groovy | 41 ---------- .../products/ProductsDbConnector.groovy | 41 ++++------ .../qbic/business/products/Converter.groovy | 79 +++++++++++++++++++ 7 files changed, 97 insertions(+), 112 deletions(-) rename offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/{products => product}/MaintainProductsController.groovy (98%) rename offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/{products => product}/MaintainProductsPresenter.groovy (96%) delete mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsView.groovy delete mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsViewModel.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/SelectItemsView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/SelectItemsView.groovy index cf4bc705b..3eb6e5962 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/SelectItemsView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/SelectItemsView.groovy @@ -267,26 +267,6 @@ class SelectItemsView extends VerticalLayout{ customerFilterRow) } - private void addDummyValues(){ - Sequencing sequencing = new Sequencing("RNA sequencing","Sequencing RNA sequences",1.4, ProductUnit.PER_SAMPLE) - Sequencing sequencing2 = new Sequencing("DNA sequencing","Sequencing DNA sequences",2.5, ProductUnit.PER_SAMPLE) - sequencingProduct = [new ProductItemViewModel(0,sequencing), new ProductItemViewModel(0,sequencing2)] - - //todo add product unit per hour? - ProjectManagement management = new ProjectManagement("Consultation","Initial consultation for a project",4,ProductUnit.PER_DATASET) - ProjectManagement management2 = new ProjectManagement("Project Design","Advising customers on how to design their project",5,ProductUnit.PER_SAMPLE) - projectManagementProduct = [new ProductItemViewModel(0,management), new ProductItemViewModel(0,management2)] - - DataStorage dataStorage = new DataStorage("Sequencing Data","Storage for all sequencing related data",3,ProductUnit.PER_GIGABYTE) - storageProduct = [new ProductItemViewModel(0,dataStorage)] - - PrimaryAnalysis primaryAnalysis = new PrimaryAnalysis("Primary analysis","Analsis of primary data",2,ProductUnit.PER_DATASET) - primaryAnalyseProduct = [new ProductItemViewModel(0,primaryAnalysis)] - - SecondaryAnalysis secondaryAnalysis = new SecondaryAnalysis("Secondary analysis","Analsis of secondary data",4,ProductUnit.PER_DATASET) - secondaryAnalyseProduct = [new ProductItemViewModel(0,secondaryAnalysis)] - } - /** * Method which generates the grid and populates the columns with the set product information from the setupDataProvider Method * diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsController.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsController.groovy similarity index 98% rename from offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsController.groovy rename to offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsController.groovy index b63a1d5e4..0dcf71e40 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsController.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsController.groovy @@ -1,4 +1,4 @@ -package life.qbic.portal.offermanager.components.products +package life.qbic.portal.offermanager.components.product import life.qbic.business.logging.Logger import life.qbic.business.logging.Logging diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsPresenter.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsPresenter.groovy similarity index 96% rename from offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsPresenter.groovy rename to offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsPresenter.groovy index 3aeb11aa8..cd97e7f18 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsPresenter.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsPresenter.groovy @@ -1,4 +1,4 @@ -package life.qbic.portal.offermanager.components.products +package life.qbic.portal.offermanager.components.product import life.qbic.business.products.archive.ArchiveProductOutput import life.qbic.business.products.create.CreateProductOutput diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsView.groovy deleted file mode 100644 index 00dd3000b..000000000 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsView.groovy +++ /dev/null @@ -1,24 +0,0 @@ -package life.qbic.portal.offermanager.components.products - -import com.vaadin.ui.FormLayout - -/** - * - *

This class generates a Form Layout in which the user can maintain the service products

- * - *

{@link MaintainProductsViewModel} will be integrated into the qOffer 2.0 Portlet and provides an User Interface - * with the intention of enabling an {@value life.qbic.portal.offermanager.security.Role#OFFER_ADMIN} to create, archive and copy products.

- * - * @since 1.0.0 - * - */ -class MaintainProductsView extends FormLayout{ - - private final MaintainProductsController controller - private final MaintainProductsViewModel viewModel - - MaintainProductsView(MaintainProductsController controller, MaintainProductsViewModel viewModel){ - this.controller = controller - this.viewModel = viewModel - } -} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsViewModel.groovy deleted file mode 100644 index fd77fce31..000000000 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/products/MaintainProductsViewModel.groovy +++ /dev/null @@ -1,41 +0,0 @@ -package life.qbic.portal.offermanager.components.products - -import life.qbic.datamodel.dtos.business.services.Product -import life.qbic.portal.offermanager.dataresources.products.ProductsResourcesService - - -/** - *

A ViewModel holding data that is presented in a - * {@link life.qbic.portal.offermanager.components.products.MaintainProductsView}

- * - *

This class holds all specific fields that are mutable in the view - * Whenever values change it should be reflected in the corresponding view. This class can be used - * for UI unit testing purposes.

- * - *

This class can contain JavaBean objects to enable views to listen to changes in the values.

- * - * @since 1.0.0 - * - */ -class MaintainProductsViewModel { - - ObservableList products = new ObservableList(new ArrayList()) - - private final ProductsResourcesService productsResourcesService - - MaintainProductsViewModel(ProductsResourcesService productsResourcesService) { - this.productsResourcesService = productsResourcesService - fetchProducts() - subscribe() - } - - private void fetchProducts(){ - products.addAll(productsResourcesService.iterator()) - } - - private void subscribe(){ - productsResourcesService.subscribe({ product -> - products << product - }) - } -} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy index 8e0534e50..ab0954178 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy @@ -2,9 +2,11 @@ package life.qbic.portal.offermanager.dataresources.products import groovy.sql.GroovyRowResult import groovy.util.logging.Log4j2 +import life.qbic.business.products.Converter import life.qbic.business.products.archive.ArchiveProductDataSource import life.qbic.business.products.create.CreateProductDataSource import life.qbic.business.products.create.ProductExistsException +import life.qbic.datamodel.dtos.business.ProductCategory import life.qbic.datamodel.dtos.business.ProductId import life.qbic.datamodel.dtos.business.ProductItem import life.qbic.datamodel.dtos.business.services.* @@ -80,47 +82,36 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData } private static Product rowResultToProduct(GroovyRowResult row) { - def productCategory = row.category + def dbProductCategory = row.category String productId = row.productId - Product product - switch(productCategory) { + ProductCategory productCategory + switch(dbProductCategory) { case "Data Storage": - product = new DataStorage(row.productName as String, - row.description as String, - row.unitPrice as Double, - new ProductUnitFactory().getForString(row.unit as String), parseProductId(productId)) + productCategory = ProductCategory.DATA_STORAGE break case "Primary Bioinformatics": - product = new PrimaryAnalysis(row.productName as String, - row.description as String, - row.unitPrice as Double, - new ProductUnitFactory().getForString(row.unit as String), parseProductId(productId)) + productCategory = ProductCategory.PRIMARY_BIOINFO break case "Project Management": - product = new ProjectManagement(row.productName as String, - row.description as String, - row.unitPrice as Double, - new ProductUnitFactory().getForString(row.unit as String), parseProductId(productId)) + productCategory = ProductCategory.PROJECT_MANAGEMENT break case "Secondary Bioinformatics": - product = new SecondaryAnalysis(row.productName as String, - row.description as String, - row.unitPrice as Double, - new ProductUnitFactory().getForString(row.unit as String), parseProductId(productId)) + productCategory = ProductCategory.SECONDARY_BIOINFO break case "Sequencing": - product = new Sequencing(row.productName as String, - row.description as String, - row.unitPrice as Double, - new ProductUnitFactory().getForString(row.unit as String), parseProductId(productId)) + productCategory = ProductCategory.SEQUENCING break } - if(product == null) { + + if(!productCategory) { log.error("Product could not be parsed from database query.") log.error(row) throw new DatabaseQueryException("Cannot parse product") } else { - return product + return Converter.createProductWithVersion(productCategory,row.productName as String, + row.description as String, + row.unitPrice as Double, + new ProductUnitFactory().getForString(row.unit as String), parseProductId(productId)) } } diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy new file mode 100644 index 000000000..9157a4533 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy @@ -0,0 +1,79 @@ +package life.qbic.business.products + +import life.qbic.datamodel.dtos.business.ProductCategory +import life.qbic.datamodel.dtos.business.services.DataStorage +import life.qbic.datamodel.dtos.business.services.MetabolomicAnalysis +import life.qbic.datamodel.dtos.business.services.PrimaryAnalysis +import life.qbic.datamodel.dtos.business.services.Product +import life.qbic.datamodel.dtos.business.services.ProductUnit +import life.qbic.datamodel.dtos.business.services.ProjectManagement +import life.qbic.datamodel.dtos.business.services.ProteomicAnalysis +import life.qbic.datamodel.dtos.business.services.SecondaryAnalysis +import life.qbic.datamodel.dtos.business.services.Sequencing + +/** + *

Converter for {@link life.qbic.datamodel.dtos.business.services.Product}

+ *
+ *

Converts a product into its respective type e.g. {@link life.qbic.datamodel.dtos.business.services.Sequencing}, + * {@link life.qbic.datamodel.dtos.business.services.ProjectManagement},..

+ * + * @since 1.0.0 + * +*/ +class Converter { + + /** + * Creates a product DTO based on its products category without a version + * + * @param category The products category which determines what kind of products is created + * @param description The description of the product + * @param name The name of the product + * @param unitPrice The unit price of the product + * @param unit The unit in which the product is measured + * @return + */ + static Product createProduct(ProductCategory category, String name, String description, double unitPrice, ProductUnit unit){ + String runningNumber = "0" + return createProductWithVersion(category,name,description,unitPrice,unit,runningNumber) + } + + /** + * Creates a product DTO based on its products category with a version + * + * @param category The products category which determines what kind of products is created + * @param description The description of the product + * @param name The name of the product + * @param unitPrice The unit price of the product + * @param unit The unit in which the product is measured + * @param runningNumber The running version number of the product + * @return + */ + static Product createProductWithVersion(ProductCategory category, String name, String description, double unitPrice, ProductUnit unit, String runningNumber){ + Product product + switch (category) { + case "DATA_STORAGE": + product = new DataStorage(name, description, unitPrice,unit, runningNumber) + break + case "PRIMARY_BIOINFO": + product = new PrimaryAnalysis(name, description, unitPrice,unit, runningNumber) + break + case "PROJECT_MANAGEMENT": + product = new ProjectManagement(name, description, unitPrice,unit, runningNumber) + break + case "SECONDARY_BIOINFO": + product = new SecondaryAnalysis(name, description, unitPrice,unit, runningNumber) + break + case "SEQUENCING": + product = new Sequencing(name, description, unitPrice,unit, runningNumber) + break + case "PROTEOMIC": + product = new ProteomicAnalysis(name, description, unitPrice,unit, runningNumber) + break + case "METABOLOMIC": + product = new MetabolomicAnalysis(name, description, unitPrice,unit, runningNumber) + break + } + if(!product) throw new IllegalArgumentException("Cannot parse products") + return product + } +} From 0884321dd48334816cf8ec2c4beed8aaf0d8d4e0 Mon Sep 17 00:00:00 2001 From: Sven F Date: Fri, 19 Mar 2021 08:55:36 +0100 Subject: [PATCH 05/32] Incorporate project link (#414) This PR links a created project with an offer and lists an associated project in the offer overview. Co-authored-by: jnnfr --- .../offermanager/DependencyManager.groovy | 5 +- .../offer/overview/OfferOverviewView.groovy | 3 + .../CreateProjectViewModel.groovy | 3 +- .../offers/OfferDbConnector.groovy | 66 ++++- .../offers/ProjectAssistant.groovy | 23 ++ .../projects/ProjectDbConnector.groovy | 244 +++++++++--------- .../projects/ProjectMainConnector.groovy | 241 +++++++++-------- 7 files changed, 327 insertions(+), 258 deletions(-) create mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/ProjectAssistant.groovy diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy index e516eac61..6b3989c56 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy @@ -202,7 +202,10 @@ class DependencyManager { openbisClient = new OpenBisClient(configurationManager.getDataSourceUser(), configurationManager.getDataSourcePassword(), openbisURL) openbisClient.login() - projectMainConnector = new ProjectMainConnector(projectDbConnector, openbisClient) + projectMainConnector = new ProjectMainConnector( + projectDbConnector, + openbisClient, + offerDbConnector) } catch (Exception e) { log.error("Unexpected exception during customer database connection.", e) diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy index 8c802d62d..b5e72fd60 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy @@ -133,6 +133,9 @@ class OfferOverviewView extends FormLayout { .setCaption("Project Title").setId("ProjectTitle") overviewGrid.addColumn({overview -> overview.getCustomer()}) .setCaption("Customer").setId("Customer") + overviewGrid.addColumn({overview -> + overview.getAssociatedProject().isPresent() ? overview.getAssociatedProject().get() : + "-"}).setCaption("Project ID").setId("ProjectID") // fix formatting of price overviewGrid.addColumn({overview -> Currency.getFormatterWithSymbol().format(overview.getTotalPrice())}).setCaption("Total Price") overviewGrid.sort(dateColumn, SortDirection.DESCENDING) diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectViewModel.groovy index 9f1f2c125..1b3f8323b 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectViewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectViewModel.groovy @@ -76,6 +76,7 @@ class CreateProjectViewModel { availableSpaces = new ListDataProvider(projectSpaceResourceService.iterator().toList()) existingProjects = projectResourceService.iterator().collect {it.projectCode} + startedFromView = Optional.empty() initFields() setupListeners() } @@ -104,11 +105,9 @@ class CreateProjectViewModel { resultingProjectCode = "" projectCodeValidationResult = "" codeIsValid = false - startedFromView = Optional.empty() createProjectEnabled = false projectCreated = false selectedOffer = Optional.empty() - } private void resetModel() { diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferDbConnector.groovy index 3862f478a..9ebab4cb6 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferDbConnector.groovy @@ -7,6 +7,9 @@ import life.qbic.datamodel.dtos.business.Customer import life.qbic.datamodel.dtos.business.Offer import life.qbic.business.exceptions.DatabaseQueryException import life.qbic.business.offers.create.CreateOfferDataSource +import life.qbic.datamodel.dtos.projectmanagement.ProjectCode +import life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier +import life.qbic.datamodel.dtos.projectmanagement.ProjectSpace import life.qbic.portal.offermanager.dataresources.persons.PersonDbConnector import life.qbic.portal.offermanager.dataresources.database.ConnectionProvider import life.qbic.portal.offermanager.dataresources.products.ProductsDbConnector @@ -24,14 +27,13 @@ import java.sql.* * */ @Log4j2 -class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource{ +class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource, ProjectAssistant{ ConnectionProvider connectionProvider PersonDbConnector customerGateway ProductsDbConnector productGateway - private static final String OFFER_INSERT_QUERY = "INSERT INTO offer (offerId, " + "creationDate, expirationDate, customerId, projectManagerId, projectTitle, " + "projectObjective, totalPrice, customerAffiliationId, vat, netPrice, overheads, " + @@ -184,7 +186,7 @@ class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource{ List offerOverviewList = [] String query = "SELECT offerId, creationDate, projectTitle, " + - "totalPrice, first_name, last_name, email\n" + + "totalPrice, first_name, last_name, email, associatedProject\n" + "FROM offer \n" + "LEFT JOIN person \n" + "ON offer.customerId = person.id" @@ -204,9 +206,19 @@ class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource{ def creationDate = resultSet.getDate("creationDate") def customerName = "${customer.getFirstName()} ${customer.getLastName()}" def offerId = parseOfferId(resultSet.getString("offerId")) - OfferOverview offerOverview = new OfferOverview(offerId, - creationDate,projectTitle, "-", - customerName, totalCosts) + Optional projectIdentifier = parseProjectIdentifier( + resultSet.getString("associatedProject")) + + OfferOverview offerOverview + if (projectIdentifier.isPresent()) { + offerOverview = new OfferOverview(offerId, + creationDate,projectTitle, + customerName, totalCosts, projectIdentifier.get()) + } else { + offerOverview = new OfferOverview(offerId, + creationDate,projectTitle, "", + customerName, totalCosts) + } offerOverviewList.add(offerOverview) } } @@ -263,8 +275,9 @@ class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource{ def selectedAffiliation = customerGateway.getAffiliation(selectedAffiliationId) def items = productGateway.getItemsForOffer(offerPrimaryId) def checksum = resultSet.getString("checksum") + def associatedProject = resultSet.getString("associatedProject") - offer = Optional.of(new Offer.Builder( + def offerBuilder = new Offer.Builder( customer, projectManager, projectTitle, @@ -279,9 +292,46 @@ class OfferDbConnector implements CreateOfferDataSource, FetchOfferDataSource{ .overheads(overheads) .netPrice(net) .checksum(checksum) - .build()) + Optional projectIdentifier = parseProjectIdentifier(associatedProject) + if (projectIdentifier.isPresent()) { + offerBuilder.associatedProject(projectIdentifier.get()) + } + offer = Optional.of(offerBuilder.build()) } } return offer } + + private static Optional parseProjectIdentifier(String projectIdentifier) { + Optional identifier = Optional.empty() + if (!projectIdentifier) { + return identifier + } + try { + def splittedIdentifier = projectIdentifier.split("/") + def space = new ProjectSpace(splittedIdentifier[0]) + def code = new ProjectCode(splittedIdentifier[1]) + identifier = Optional.of(new ProjectIdentifier(space, code)) + } catch (Exception e) { + log.error(e.message) + log.error(e.stackTrace.join("\n")) + } + return identifier + } + + /** + * {@inheritDocs} + */ + @Override + void linkOfferWithProject(OfferId offerId, ProjectIdentifier projectIdentifier) { + String query = "UPDATE offer SET associatedProject = ? WHERE offerId = ?" + + Connection connection = connectionProvider.connect() + connection.withCloseable { + PreparedStatement statement = it.prepareStatement(query) + statement.setString(1, projectIdentifier.toString()) + statement.setString(2, offerId.toString()) + statement.executeUpdate() + } + } } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/ProjectAssistant.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/ProjectAssistant.groovy new file mode 100644 index 000000000..8a9cf068b --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/ProjectAssistant.groovy @@ -0,0 +1,23 @@ +package life.qbic.portal.offermanager.dataresources.offers + +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.business.OfferId +import life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier + +/** + * Small helper interface that provides linking functionality of offers and projects + * + * @since 1.0.0 + */ +interface ProjectAssistant { + + /** + * Link an offer with an associated project id. + * @param offerId The offer you want to link to the project + * @param projectIdentifier The project you want to have the offer linked to + * @throws DatabaseQueryException if the update of the offer entry cannot be performed + */ + void linkOfferWithProject(OfferId offerId, ProjectIdentifier projectIdentifier) + throws DatabaseQueryException + +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectDbConnector.groovy index ec4658ee6..07d19a053 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectDbConnector.groovy @@ -1,7 +1,7 @@ package life.qbic.portal.offermanager.dataresources.projects import groovy.util.logging.Log4j2 -import life.qbic.datamodel.dtos.general.Person + import life.qbic.portal.offermanager.dataresources.persons.PersonDbConnector import life.qbic.datamodel.dtos.business.* @@ -25,166 +25,162 @@ import java.sql.Statement @Log4j2 class ProjectDbConnector { - /** + /** * A connection to the project/customer database used to create queries. */ private final ConnectionProvider connectionProvider - /** + /** * A connector to the customer database used to create queries. */ private final PersonDbConnector personDbConnector - /** + /** * Constructor for a ProjectDbConnector * @param connectionProvider a connection provider to the project/customer db * @param personDbConnector db connector used to connect projects to customer and manager * */ ProjectDbConnector(ConnectionProvider connectionProvider, PersonDbConnector personDbConnector) { - this.connectionProvider = connectionProvider - this.personDbConnector = personDbConnector + this.connectionProvider = connectionProvider + this.personDbConnector = personDbConnector } - /** + /** * parses existing projects from user database, might be needed later if more complex information is to be listed */ - public List fetchProjects() { - List projects = [] - String query = "SELECT openbis_project_identifier from projects" - Connection connection = connectionProvider.connect() - connection.withCloseable { - def preparedStatement = it.prepareStatement(query) - ResultSet resultSet = preparedStatement.executeQuery() - while(resultSet.next()) { - try { - String[] tokens = resultSet.getString('openbis_project_identifier').split("/") - ProjectSpace space = tokens[1] - ProjectCode project = tokens[2] - } catch (Exception e) { - e.printStackTrace() - throw new DatabaseQueryException("Could not parse existing projects from database.") - } - projects.add(new ProjectIdentifier(space, project)) + List fetchProjects() { + List projects = [] + String query = "SELECT openbis_project_identifier from projects" + Connection connection = connectionProvider.connect() + connection.withCloseable { + def preparedStatement = it.prepareStatement(query) + ResultSet resultSet = preparedStatement.executeQuery() + while(resultSet.next()) { + try { + String[] tokens = resultSet.getString('openbis_project_identifier').split("/") + ProjectSpace space = new ProjectSpace(tokens[1]) + ProjectCode project = new ProjectCode(tokens[2]) + projects.add(new ProjectIdentifier(space, project)) + } catch (Exception e) { + e.printStackTrace() + throw new DatabaseQueryException("Could not parse existing projects from database.") + } + } } - } - return projects + return projects } - /** + /** * Add a project to the user database to connect additional metadata that is not stored in openBIS * The project is uniquely recognizable by its openBIS project identifier, containing space and * project code * @param projectIdentifier a project identifier object denoting the openBIS identifier * @param projectApplication a project application object used to add additional metadata */ - public Project addProjectAndConnectPersonsInUserDB(projectIdentifier, projectApplication) { - //collect infos needed for database - String projectTitle = projectApplication.getProjectTitle() - Customer customer = projectApplication.getCustomer() - ProjectManager projectManager = projectApplication.getProjectManager() - - //fetch needed person ids from database - int customerID = personDBConnector.getPersonId(customer) - int managerID = personDBConnector.getPersonId(projectManager) - - Connection connection = connectionProvider.connect() - connection.setAutoCommit(false) - - connection.withCloseable {it -> - try { - int projectID = addProjectToDB(it, projectIdentifier, projectTitle) - addPersonToProject(it, projectID, managerID, "Manager") - addPersonToProject(it, projectID, customerID, "PI") + Project addProjectAndConnectPersonsInUserDB(ProjectIdentifier projectIdentifier, + ProjectApplication projectApplication) { + //collect infos needed for database + String projectTitle = projectApplication.getProjectTitle() + Customer customer = projectApplication.getCustomer() + ProjectManager projectManager = projectApplication.getProjectManager() - it.commit() - - } catch (Exception e) { - log.error(e.message) - log.error(e.stackTrace.join("\n")) - it.rollback() - - throw new DatabaseQueryException("Could not add person and project data to user database.") + //fetch needed person ids from database + int customerID = personDbConnector.getPersonId(customer) + int managerID = personDbConnector.getPersonId(projectManager) + + Connection connection = connectionProvider.connect() + connection.setAutoCommit(false) + + connection.withCloseable {it -> + try { + int projectID = addProjectToDB(it, projectIdentifier.toString(), projectTitle) + addPersonToProject(it, projectID, managerID, "Manager") + addPersonToProject(it, projectID, customerID, "PI") + + it.commit() + + } catch (Exception e) { + log.error(e.message) + log.error(e.stackTrace.join("\n")) + it.rollback() + + throw new DatabaseQueryException("Could not add person and project data to user database.") + } } - } - return new Project(projectIdentifier, projectTitle, projectApplication.getLinkedOffer()) + return new Project.Builder(projectIdentifier, projectTitle) + .linkedOfferId(projectApplication.linkedOffer).build() } - private boolean isProjectInDB(String projectIdentifier) { - log.debug("Looking for project " + projectIdentifier + " in the DB"); - String sql = "SELECT * from projects WHERE openbis_project_identifier = ?"; - Connection connection = connectionProvider.connect() - connection.withCloseable { it -> - PreparedStatement statement = it.prepareStatement(sql); - statement.setString(1, projectIdentifier); - ResultSet rs = statement.executeQuery(); - if (rs.next()) { - return true + private boolean isProjectInDB(String projectIdentifier) { + String sql = "SELECT * from projects WHERE openbis_project_identifier = ?" + Connection connection = connectionProvider.connect() + connection.withCloseable { it -> + PreparedStatement statement = it.prepareStatement(sql) + statement.setString(1, projectIdentifier) + ResultSet rs = statement.executeQuery() + if (rs.next()) { + return true + } } - } - return false; + return false } - private int addProjectToDB(Connection connection, String projectIdentifier, String projectName) { - if(isProjectInDB(projectIdentifier)) { - throw new ProjectExistsException("Project "+projectIdentifier+" is already in the user database") - } - log.debug("Trying to add project " + projectIdentifier + " to the person DB"); - String sql = "INSERT INTO projects (openbis_project_identifier, short_title) VALUES(?, ?)"; - try (PreparedStatement statement = - connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { - statement.setString(1, projectIdentifier); - statement.setString(2, projectName); - statement.execute(); - ResultSet rs = statement.getGeneratedKeys(); - if (rs.next()) { - logout(conn); - log.debug("Successful."); - return rs.getInt(1); + private int addProjectToDB(Connection connection, String projectIdentifier, String projectName) { + if(isProjectInDB(projectIdentifier)) { + throw new ProjectExistsException("Project "+projectIdentifier+" is already in the user database") + } + log.debug("Trying to add project " + projectIdentifier + " to the person DB") + String sql = "INSERT INTO projects (openbis_project_identifier, short_title) VALUES(?, ?)" + try (PreparedStatement statement = + connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + statement.setString(1, projectIdentifier) + statement.setString(2, projectName) + statement.execute() + ResultSet rs = statement.getGeneratedKeys() + if (rs.next()) { + log.debug("Successful.") + return rs.getInt(1) + } } - } - return -1 + return -1 } - private void addPersonToProject(Connection connection, int projectID, int personID, String role) { - if (!hasPersonRoleInProject(personID, projectID, role)) { - log.debug("Trying to add person with role " + role + " to a project."); - String sql = - "INSERT INTO projects_persons (project_id, person_id, project_role) VALUES(?, ?, ?)"; - try (PreparedStatement statement = - connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { - statement.setInt(1, projectID); - statement.setInt(2, personID); - statement.setString(3, role); - statement.execute(); - log.debug("Successful."); - } catch (Exception e) { - log.error("SQL operation unsuccessful: " + e.getMessage()); - e.printStackTrace(); - } + private void addPersonToProject(Connection connection, int projectID, int personID, String role) { + if (!hasPersonRoleInProject(personID, projectID, role)) { + log.debug("Trying to add person with role " + role + " to a project.") + String sql = + "INSERT INTO projects_persons (project_id, person_id, project_role) VALUES(?, ?, ?)"; + try (PreparedStatement statement = + connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + statement.setInt(1, projectID) + statement.setInt(2, personID) + statement.setString(3, role) + statement.execute() + } catch (Exception e) { + log.error("SQL operation unsuccessful: " + e.getMessage()) + e.printStackTrace() + } + } } - } - - private boolean hasPersonRoleInProject(int personID, int projectID, String role) { - logger.info("Checking if person already has this role in the project."); - String sql = - "SELECT * from projects_persons WHERE person_id = ? AND project_id = ? and project_role = ?"; - boolean res = false; - Connection connection = connectionProvider.connect() - try { - PreparedStatement statement = connection.prepareStatement(sql); - statement.setInt(1, personID); - statement.setInt(2, projectID); - statement.setString(3, role); - ResultSet rs = statement.executeQuery(); - if (rs.next()) { - res = true; - logger.info("person already has this role!"); + + private boolean hasPersonRoleInProject(int personID, int projectID, String role) { + String sql = + "SELECT * from projects_persons WHERE person_id = ? AND project_id = ? and project_role = ?" + boolean res = false + Connection connection = connectionProvider.connect() + try { + PreparedStatement statement = connection.prepareStatement(sql) + statement.setInt(1, personID) + statement.setInt(2, projectID) + statement.setString(3, role) + ResultSet rs = statement.executeQuery() + if (rs.next()) { + res = true + } + } catch (Exception e) { + log.error("SQL operation unsuccessful: " + e.getMessage()) + e.printStackTrace() } - } catch (Exception e) { - logger.error("SQL operation unsuccessful: " + e.getMessage()); - e.printStackTrace(); - } - logout(conn); - return res; + return res } } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectMainConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectMainConnector.groovy index b5ba78f3c..363ca29ff 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectMainConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectMainConnector.groovy @@ -14,11 +14,6 @@ import life.qbic.datamodel.dtos.business.* import life.qbic.datamodel.dtos.projectmanagement.* import life.qbic.business.exceptions.DatabaseQueryException -import java.sql.Connection -import java.sql.PreparedStatement -import java.sql.ResultSet -import java.sql.Statement - import life.qbic.openbis.openbisclient.OpenBisClient import ch.ethz.sis.openbis.generic.asapi.v3.dto.operation.SynchronousOperationExecutionOptions @@ -28,6 +23,7 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.project.create.ProjectCreation import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.create.CreateSpacesOperation import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.create.SpaceCreation import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.id.SpacePermId +import life.qbic.portal.offermanager.dataresources.offers.ProjectAssistant /** @@ -37,147 +33,146 @@ import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.id.SpacePermId * transferring data to the project/customer db and openBIS * * @since 1.0.0 - * */ @Log4j2 @CompileStatic class ProjectMainConnector implements CreateProjectDataSource, CreateProjectSpaceDataSource { - /** - * A connection to the project (and customer) database used to create queries. - */ - private final ProjectDbConnector projectDbConnector - private final OpenBisClient openbisClient - private List openbisSpaces - private List openbisProjects + /** + * A connection to the project (and customer) database used to create queries. + */ + private final ProjectDbConnector projectDbConnector + private final OpenBisClient openbisClient + private final ProjectAssistant projectAssistant + private List openbisSpaces + private List openbisProjects - /** + /** * Constructor for a ProjectMainConnector * @param projectDbConnector a connector enabling interaction with the project database * @param openbisClient an openBIS client API object */ - ProjectMainConnector(ProjectDbConnector projectDbConnector, OpenBisClient openbisClient) { - this.projectDbConnector = projectDbConnector - this.openbisClient = openbisClient - fetchExistingSpaces() - fetchExistingProjects() - } - - private void fetchExistingSpaces() { - this.openbisSpaces = new ArrayList<>() - for(String spaceName : openbisClient.listSpaces()) { - this.openbisSpaces.add(new ProjectSpace(spaceName)) + ProjectMainConnector(ProjectDbConnector projectDbConnector, + OpenBisClient openbisClient, + ProjectAssistant projectAssistant) { + this.projectDbConnector = projectDbConnector + this.openbisClient = openbisClient + this.projectAssistant = projectAssistant + fetchExistingSpaces() + fetchExistingProjects() + } + + private void fetchExistingSpaces() { + this.openbisSpaces = new ArrayList<>() + for (String spaceName : openbisClient.listSpaces()) { + this.openbisSpaces.add(new ProjectSpace(spaceName)) + } + } + + /** + * Returns a copy of the list of available project spaces that has been fetched from openBIS upon creation of this class instance + */ + List listSpaces() { + return new ArrayList(openbisSpaces) } - } - - /** - * Returns a copy of the list of available project spaces that has been fetched from openBIS upon creation of this class instance - */ - public List listSpaces() { - return new ArrayList(openbisSpaces); - } - - private void fetchExistingProjects() { - //projectDbConnector.fetchProjects() might be used at some point to fetch more metadata - - openbisProjects = [] - for(ch.ethz.sis.openbis.generic.asapi.v3.dto.project.Project openbisProject : openbisClient.listProjects()) { - try { - ProjectSpace space = new ProjectSpace(openbisProject.getSpace().getCode()) - ProjectCode code = new ProjectCode(openbisProject.getCode()) - openbisProjects.add(new ProjectIdentifier(space, code)) - } catch (Exception e) { - log.error(e.message) - } + + private void fetchExistingProjects() { + //projectDbConnector.fetchProjects() might be used at some point to fetch more metadata + + openbisProjects = [] + for (ch.ethz.sis.openbis.generic.asapi.v3.dto.project.Project openbisProject : openbisClient.listProjects()) { + try { + ProjectSpace space = new ProjectSpace(openbisProject.getSpace().getCode()) + ProjectCode code = new ProjectCode(openbisProject.getCode()) + openbisProjects.add(new ProjectIdentifier(space, code)) + } catch (Exception e) { + log.error(e.message) + } + } } - } - private void createOpenbisSpace(String spaceName, String description) { - SpaceCreation space = new SpaceCreation() - space.setCode(spaceName) + private void createOpenbisSpace(String spaceName, String description) { + SpaceCreation space = new SpaceCreation() + space.setCode(spaceName) - space.setDescription(description) + space.setDescription(description) - IOperation operation = new CreateSpacesOperation(space) - handleOperations(operation) - } + IOperation operation = new CreateSpacesOperation(space) + handleOperations(operation) + } - private void createOpenbisProject(ProjectSpace space, ProjectCode projectCode, String description) { - ProjectCreation project = new ProjectCreation(); - project.setCode(projectCode.toString()); - project.setSpaceId(new SpacePermId(space.toString())); - project.setDescription(description); + private void createOpenbisProject(ProjectSpace space, ProjectCode projectCode, String description) { + ProjectCreation project = new ProjectCreation() + project.setCode(projectCode.toString()) + project.setSpaceId(new SpacePermId(space.toString())) + project.setDescription(description) - IOperation operation = new CreateProjectsOperation(project); - handleOperations(operation); - } + IOperation operation = new CreateProjectsOperation(project) + handleOperations(operation) + } - /** - * Returns a copied list of existing projects fetched upon creation of this class - */ - public List fetchProjects() { - return new ArrayList(openbisProjects); - } + /** + * Returns a copied list of existing projects fetched upon creation of this class + */ + List fetchProjects() { + return new ArrayList(openbisProjects) + } - @Override + @Override void createProjectSpace(ProjectSpace projectSpace) throws ProjectSpaceExistsException, DatabaseQueryException { - String spaceName = projectSpace.getName() - if(openbisClient.spaceExists(spaceName)) { - throw new ProjectSpaceExistsException("Project space "+spaceName+" could not be created, as it exists in openBIS already!") - } - try { - //we don't provide a description in our data model for now, but it's optional anyway - createOpenbisSpace(spaceName, "") - - } catch (Exception e) { - log.error(e.message) - log.error(e.stackTrace.join("\n")) - throw new DatabaseQueryException("Could not create project space.") - } + String spaceName = projectSpace.getName() + if (openbisClient.spaceExists(spaceName)) { + throw new ProjectSpaceExistsException("Project space " + spaceName + " could not be created, as it exists in openBIS already!") + } + try { + //we don't provide a description in our data model for now, but it's optional anyway + createOpenbisSpace(spaceName, "") + + } catch (Exception e) { + log.error(e.message) + log.error(e.stackTrace.join("\n")) + throw new DatabaseQueryException("Could not create project space.") + } } - @Override + @Override Project createProject(ProjectApplication projectApplication) throws ProjectExistsException, DatabaseQueryException { - //collect infos needed for openBIS - ProjectSpace space = projectApplication.getProjectSpace() - ProjectCode projectCode = projectApplication.getProjectCode() - String description = projectApplication.getProjectObjective() - - ProjectIdentifier projectIdentifier = new ProjectIdentifier(space, projectCode) - - //collect infos needed for database - String projectTitle = projectApplication.getProjectTitle() - Customer customer = projectApplication.getCustomer() - ProjectManager projectManager = projectApplication.getProjectManager() - - //if the space does not exist, an error shall be thrown - if (!openbisClient.spaceExists(space.toString())) { - throw new SpaceNonExistingException("Could not create project because of non-existent space: "+space.toString()) - } - if (openbisClient.projectExists(space.toString(), projectCode.toString())) { - throw new ProjectExistsException("Project "+projectIdentifier.toString()+" could not be created, as it exists in openBIS already!") - } - try { - createOpenbisProject(space, projectCode, description) - } catch (Exception e) { - log.error(e.message) - log.error(e.stackTrace.join("\n")) - throw new DatabaseQueryException("Could not create project.") - } - - return projectDbConnector.addProjectAndConnectPersonsInUserDB(projectIdentifier, projectApplication) + //collect infos needed for openBIS + ProjectSpace space = projectApplication.getProjectSpace() + ProjectCode projectCode = projectApplication.getProjectCode() + String description = projectApplication.getProjectObjective() + + ProjectIdentifier projectIdentifier = new ProjectIdentifier(space, projectCode) + + //if the space does not exist, an error shall be thrown + if (!openbisClient.spaceExists(space.toString())) { + throw new SpaceNonExistingException("Could not create project because of non-existent space: " + space.toString()) + } + if (openbisClient.projectExists(space.toString(), projectCode.toString())) { + throw new ProjectExistsException("Project " + projectIdentifier.toString() + " could not be created, as it exists in openBIS already!") + } + try { + createOpenbisProject(space, projectCode, description) + projectAssistant.linkOfferWithProject(projectApplication.linkedOffer, projectIdentifier) + } catch (Exception e) { + log.error(e.message) + log.error(e.stackTrace.join("\n")) + throw new DatabaseQueryException("Could not create project.") + } + + return projectDbConnector.addProjectAndConnectPersonsInUserDB(projectIdentifier, projectApplication) } - - private void handleOperations(IOperation operation) { - IApplicationServerApi api = openbisClient.getV3() - - SynchronousOperationExecutionOptions executionOptions = new SynchronousOperationExecutionOptions() - List operationOptions = Arrays.asList(operation) - try { - api.executeOperations(openbisClient.getSessionToken(), operationOptions, executionOptions) - } catch (Exception e) { - log.error("Unexpected exception during openBIS operation.", e) - throw e + + private void handleOperations(IOperation operation) { + IApplicationServerApi api = openbisClient.getV3() + + SynchronousOperationExecutionOptions executionOptions = new SynchronousOperationExecutionOptions() + List operationOptions = Arrays.asList(operation) + try { + api.executeOperations(openbisClient.getSessionToken(), operationOptions, executionOptions) + } catch (Exception e) { + log.error("Unexpected exception during openBIS operation.", e) + throw e + } } - } } From 55dbb413b6592629f441edf268f8bb9b8703068d Mon Sep 17 00:00:00 2001 From: jnnfr Date: Fri, 19 Mar 2021 11:30:50 +0100 Subject: [PATCH 06/32] Connect CreateProduct and ArchiveProduct (#411) Connects the create and archive product use cases with the view Co-authored-by: Steffengreiner --- CHANGELOG.rst | 5 ++ .../offermanager/DependencyManager.groovy | 51 +++++++++++-- .../offer/create/CreateOfferViewModel.groovy | 8 +- .../product/MaintainProductsController.groovy | 26 +------ .../product/MaintainProductsPresenter.groovy | 2 + .../product/MaintainProductsView.groovy | 24 +++--- .../product/MaintainProductsViewModel.groovy | 12 ++- .../product/create/CreateProductView.groovy | 76 ++++++++++--------- .../products/ProductsDbConnector.groovy | 11 +-- .../ProductResourceServiceSpec.groovy | 49 ++++++++++++ .../products/create/CreateProduct.groovy | 1 + 11 files changed, 179 insertions(+), 86 deletions(-) create mode 100644 offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/products/ProductResourceServiceSpec.groovy diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7e3acf813..d5f55b43c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog This project adheres to `Semantic Versioning `_. + 1.0.0-alpha.5-SNAPSHOT (2021-03-17) -------------------------- @@ -13,6 +14,10 @@ This project adheres to `Semantic Versioning `_. .offermanager.dataresources.offers`` have been extended with a new property to associate it with an existing project by its project identifier. +* Finalized the ``life.qbic.business.products.archive.ArchiveProduct`` and ``life/qbic/business/products/create/CreateProduct.groovy`` +use cases of the product maintenance and creation feature(`#411 `_). + + **Fixed** **Dependencies** diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy index 6b3989c56..2217f619b 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy @@ -2,6 +2,8 @@ package life.qbic.portal.offermanager import groovy.util.logging.Log4j2 import life.qbic.business.offers.fetch.FetchOffer +import life.qbic.business.products.archive.ArchiveProduct +import life.qbic.business.products.create.CreateProduct import life.qbic.business.projects.create.CreateProject import life.qbic.datamodel.dtos.business.AcademicTitle import life.qbic.datamodel.dtos.business.AffiliationCategory @@ -20,10 +22,12 @@ import life.qbic.portal.offermanager.components.offer.overview.projectcreation.C import life.qbic.portal.offermanager.components.person.search.SearchPersonView import life.qbic.portal.offermanager.components.person.search.SearchPersonViewModel import life.qbic.portal.offermanager.components.person.update.UpdatePersonViewModel +import life.qbic.portal.offermanager.components.product.MaintainProductsPresenter import life.qbic.portal.offermanager.components.product.MaintainProductsView import life.qbic.portal.offermanager.components.product.MaintainProductsViewModel import life.qbic.portal.offermanager.components.product.create.CreateProductView import life.qbic.portal.offermanager.components.product.create.CreateProductViewModel +import life.qbic.portal.offermanager.components.product.MaintainProductsController import life.qbic.portal.offermanager.dataresources.persons.AffiliationResourcesService import life.qbic.portal.offermanager.dataresources.persons.PersonDbConnector import life.qbic.portal.offermanager.dataresources.persons.CustomerResourceService @@ -93,6 +97,7 @@ class DependencyManager { private SearchPersonViewModel searchPersonViewModel private CreatePersonViewModel createCustomerViewModelNewOffer private MaintainProductsViewModel maintainProductsViewModel + private MaintainProductsViewModel maintainProductsViewModelArchive private CreateProductViewModel createProductViewModel private CreateProductViewModel copyProductViewModel private CreateProjectViewModel createProjectModel @@ -105,6 +110,8 @@ class DependencyManager { private CreateOfferPresenter createOfferPresenter private CreateOfferPresenter updateOfferPresenter private OfferOverviewPresenter offerOverviewPresenter + private MaintainProductsPresenter createProductPresenter + private MaintainProductsPresenter archiveProductPresenter private CreateProjectPresenter createProjectPresenter private PersonDbConnector customerDbConnector @@ -124,6 +131,8 @@ class DependencyManager { private FetchOffer fetchOfferOfferOverview private FetchOffer fetchOfferCreateOffer private FetchOffer fetchOfferUpdateOffer + private CreateProduct createProduct + private ArchiveProduct archiveProduct private CreatePersonController createCustomerController private CreatePersonController updateCustomerController @@ -132,6 +141,7 @@ class DependencyManager { private CreateOfferController createOfferController private CreateOfferController updateOfferController private OfferOverviewController offerOverviewController + private MaintainProductsController maintainProductController private CreateProjectController createProjectController private CreatePersonView createCustomerView @@ -333,6 +343,12 @@ class DependencyManager { log.error("Unexpected exception during ${MaintainProductsViewModel.getSimpleName()} view model setup.", e) } + try { + this.maintainProductsViewModelArchive = new MaintainProductsViewModel(productsResourcesService) + }catch (Exception e) { + log.error("Unexpected exception during ${MaintainProductsViewModel.getSimpleName()} view model setup.", e) + } + try { this.createProductViewModel = new CreateProductViewModel() }catch (Exception e) { @@ -400,10 +416,21 @@ class DependencyManager { } catch (Exception e) { log.error("Unexpected exception during ${OfferOverviewPresenter.getSimpleName()} setup", e) } + + try { + this.createProductPresenter = new MaintainProductsPresenter(this.maintainProductsViewModel, this.viewModel) + } catch (Exception e) { + log.error("Unexpected exception during ${MaintainProductsPresenter.getSimpleName()} setup", e) + } + try { + this.archiveProductPresenter = new MaintainProductsPresenter(this.maintainProductsViewModelArchive, this.viewModel) + } catch (Exception e) { + log.error("Unexpected exception during ${MaintainProductsPresenter.getSimpleName()} setup", e) + } try { this.createProjectPresenter = new CreateProjectPresenter(createProjectModel, viewModel) } catch (Exception e) { - log.error("Unexpected exception during ${OfferOverviewPresenter.getSimpleName()} setup", e) + log.error("Unexpected exception during ${CreateProjectPresenter.getSimpleName()} setup", e) } } @@ -421,6 +448,9 @@ class DependencyManager { this.fetchOfferCreateOffer = new FetchOffer(offerDbConnector, createOfferPresenter) this.fetchOfferUpdateOffer = new FetchOffer(offerDbConnector, updateOfferPresenter) + this.createProduct = new CreateProduct(productsDbConnector,createProductPresenter) + this.archiveProduct = new ArchiveProduct(productsDbConnector,archiveProductPresenter) + this.createProject = new CreateProject(createProjectPresenter, projectMainConnector, projectMainConnector) } @@ -464,10 +494,17 @@ class DependencyManager { } catch (Exception e) { log.error("Unexpected exception during ${OfferOverviewController.getSimpleName()} setup", e) } + try { + this.maintainProductController = new MaintainProductsController(this.createProduct, this.archiveProduct) + } catch (Exception e) { + log.error("Unexpected exception during ${MaintainProductsController.getSimpleName()} setup", e) + } + + try{ this.createProjectController = new CreateProjectController(this.createProject) } catch (Exception e) { - log.error("Unexpected exception during ${OfferOverviewController.getSimpleName()} setup", e) + log.error("Unexpected exception during ${CreateProjectController.getSimpleName()} setup", e) } } @@ -555,23 +592,23 @@ class DependencyManager { CreateProductView createProductView try{ - createProductView = new CreateProductView(createProductViewModel) + createProductView = new CreateProductView(createProductViewModel,maintainProductController) }catch(Exception e){ log.error("Could not create ${CreateProductView.getSimpleName()} view.", e) throw e } - CreateProductView copyProductView + /**CreateProductView copyProductView try{ - copyProductView = new CreateProductView(copyProductViewModel) + copyProductView = new CreateProductView(copyProductViewModel,maintainProductController) }catch(Exception e){ log.error("Could not create ${CreateProductView.getSimpleName()} view.", e) throw e - } + }**/ MaintainProductsView maintainProductsView try{ - maintainProductsView = new MaintainProductsView(maintainProductsViewModel,createProductView,copyProductView) + maintainProductsView = new MaintainProductsView(maintainProductsViewModel,createProductView,maintainProductController) }catch (Exception e) { log.error("Could not create ${MaintainProductsView.getSimpleName()} view.", e) throw e diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy index 65f16bfe7..e5e359e5b 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy @@ -90,13 +90,17 @@ class CreateOfferViewModel { Subscription productSubscription = new Subscription() { @Override void receive(Product product) { - List products = productsResourcesService.iterator().toList() - populateProductLists(products) + refreshProducts() } } this.productsResourcesService.subscribe(productSubscription) } + private void refreshProducts(){ + List products = productsResourcesService.iterator().toList() + populateProductLists(products) + } + private void populateProductLists(List products) { this.sequencingProducts.clear() this.managementProducts.clear() diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsController.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsController.groovy index 0dcf71e40..6fd78de11 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsController.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsController.groovy @@ -2,6 +2,7 @@ package life.qbic.portal.offermanager.components.product import life.qbic.business.logging.Logger import life.qbic.business.logging.Logging +import life.qbic.business.products.Converter import life.qbic.business.products.archive.ArchiveProductInput import life.qbic.business.products.create.CreateProductInput @@ -63,7 +64,7 @@ class MaintainProductsController { archiveProductInput.archive(productId) }catch(Exception unexpected){ log.error("unexpected exception at archive product call", unexpected) - throw new IllegalArgumentException("Could not create products from provided arguments.") + throw new IllegalArgumentException("Could not archive products from provided arguments.") } } @@ -80,28 +81,7 @@ class MaintainProductsController { * @return */ static Product createProduct(ProductCategory category, String description, String name, double unitPrice, ProductUnit unit){ - Product product - switch (category) { - case "Data Storage": - //todo do we want to set the id manually to null or update the DTO constructor? - product = new DataStorage(name, description, unitPrice,unit, null) - break - case "Primary Bioinformatics": - product = new PrimaryAnalysis(name, description, unitPrice,unit, null) - break - case "Project Management": - product = new ProjectManagement(name, description, unitPrice,unit, null) - break - case "Secondary Bioinformatics": - product = new SecondaryAnalysis(name, description, unitPrice,unit, null) - break - case "Sequencing": - product = new Sequencing(name, description, unitPrice,unit, null) - break - } - if(!product) throw new IllegalArgumentException("Cannot parse products") - - return product + return Converter.createProduct(category,name, description, unitPrice,unit) } } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsPresenter.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsPresenter.groovy index cd97e7f18..8963b33ff 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsPresenter.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsPresenter.groovy @@ -26,11 +26,13 @@ class MaintainProductsPresenter implements CreateProductOutput, ArchiveProductOu @Override void archived(Product product) { mainViewModel.successNotifications << "Successfully archived product $product.productId - $product.productName." + productsViewModel.productsResourcesService.removeFromResource(product) } @Override void created(Product product) { mainViewModel.successNotifications << "Successfully added new product $product.productId - $product.productName." + productsViewModel.productsResourcesService.addToResource(product) } @Override diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsView.groovy index 8632f73d5..edb96240f 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsView.groovy @@ -12,6 +12,7 @@ import com.vaadin.ui.VerticalLayout import com.vaadin.ui.components.grid.HeaderRow import com.vaadin.ui.themes.ValoTheme import life.qbic.business.offers.Currency +import life.qbic.business.products.archive.ArchiveProduct import life.qbic.datamodel.dtos.business.services.Product import life.qbic.portal.offermanager.components.GridUtils import life.qbic.portal.offermanager.components.product.create.CreateProductView @@ -30,6 +31,7 @@ import life.qbic.portal.offermanager.dataresources.offers.OfferOverview class MaintainProductsView extends VerticalLayout{ private final MaintainProductsViewModel viewModel + private final MaintainProductsController controller Grid productGrid HorizontalLayout buttonLayout @@ -40,14 +42,12 @@ class MaintainProductsView extends VerticalLayout{ VerticalLayout maintenanceLayout CreateProductView createProductView - CreateProductView copyProductView - MaintainProductsView(MaintainProductsViewModel viewModel, CreateProductView createProductView - , CreateProductView copyProductView){ - //todo add the controller + MaintainProductsView(MaintainProductsViewModel viewModel, CreateProductView createProductView, + MaintainProductsController controller){ + this.controller = controller this.viewModel = viewModel this.createProductView = createProductView - this.copyProductView = copyProductView setupPanel() createButtons() @@ -68,6 +68,7 @@ class MaintainProductsView extends VerticalLayout{ private void createButtons(){ addProduct = new Button("Add Product", VaadinIcons.PLUS) copyProduct = new Button ("Copy Product", VaadinIcons.COPY) + copyProduct.setEnabled(false) archiveProduct = new Button("Archive Product", VaadinIcons.ARCHIVE) buttonLayout = new HorizontalLayout(productDescription, addProduct,copyProduct,archiveProduct) @@ -123,9 +124,8 @@ class MaintainProductsView extends VerticalLayout{ } private void addSubViews(){ - this.addComponents(createProductView,copyProductView) + this.addComponents(createProductView) //todo add the copy product use case view here createProductView.setVisible(false) - copyProductView.setVisible(false) } private void updateProductDescription(Product product){ @@ -143,6 +143,7 @@ class MaintainProductsView extends VerticalLayout{ productGrid.addSelectionListener({ if(it.firstSelectedItem.isPresent()){ updateProductDescription(it.firstSelectedItem.get()) + viewModel.selectedProduct = it.firstSelectedItem.get() } }) @@ -156,13 +157,12 @@ class MaintainProductsView extends VerticalLayout{ createProductView.setVisible(false) }) - copyProduct.addClickListener({ - maintenanceLayout.setVisible(false) - copyProduct.setVisible(true) + archiveProduct.addClickListener({ + controller.archiveProduct(viewModel.selectedProduct.productId) }) - archiveProduct.addClickListener({ - //todo use the controller to trigger the use case + viewModel.products.addPropertyChangeListener({ + productGrid.dataProvider.refreshAll() }) } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsViewModel.groovy index a74ea420e..5bf02c8b8 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsViewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsViewModel.groovy @@ -1,5 +1,6 @@ package life.qbic.portal.offermanager.components.product +import groovy.beans.Bindable import life.qbic.datamodel.dtos.business.services.Product import life.qbic.portal.offermanager.dataresources.products.ProductsResourcesService @@ -21,9 +22,9 @@ class MaintainProductsViewModel { ObservableList products = new ObservableList(new ArrayList()) - Product selectedProduct + @Bindable Product selectedProduct - private final ProductsResourcesService productsResourcesService + final ProductsResourcesService productsResourcesService MaintainProductsViewModel(ProductsResourcesService productsResourcesService) { this.productsResourcesService = productsResourcesService @@ -37,7 +38,12 @@ class MaintainProductsViewModel { private void subscribe(){ productsResourcesService.subscribe({ product -> - products << product + refreshList() }) } + + private void refreshList(){ + products.clear() + products.addAll(productsResourcesService.iterator().toList()) + } } \ No newline at end of file diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/create/CreateProductView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/create/CreateProductView.groovy index 96ed1c404..76d11d60f 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/create/CreateProductView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/create/CreateProductView.groovy @@ -16,6 +16,7 @@ import com.vaadin.ui.VerticalLayout import com.vaadin.ui.themes.ValoTheme import life.qbic.datamodel.dtos.business.ProductCategory import life.qbic.datamodel.dtos.business.services.ProductUnit +import life.qbic.portal.offermanager.components.product.MaintainProductsController /** *

This view serves the user to create a new service product

@@ -27,7 +28,8 @@ import life.qbic.datamodel.dtos.business.services.ProductUnit */ class CreateProductView extends HorizontalLayout{ - private final CreateProductViewModel createProductViewModel + private final CreateProductViewModel viewModel + private final MaintainProductsController controller TextField productNameField TextField productDescriptionField @@ -39,9 +41,10 @@ class CreateProductView extends HorizontalLayout{ Button createProductButton Button abortButton - CreateProductView(CreateProductViewModel createProductViewModel){ + CreateProductView(CreateProductViewModel createProductViewModel, MaintainProductsController controller){ + this.controller = controller + this.viewModel = createProductViewModel - this.createProductViewModel = createProductViewModel initTextFields() initComboBoxes() initButtons() @@ -113,29 +116,29 @@ class CreateProductView extends HorizontalLayout{ private void bindViewModel(){ //bind all textfields - this.productNameField.addValueChangeListener({this.createProductViewModel.productName = it.value }) + this.productNameField.addValueChangeListener({this.viewModel.productName = it.value }) - createProductViewModel.addPropertyChangeListener("productName", { + viewModel.addPropertyChangeListener("productName", { String newValue = it.newValue as String productNameField.value = newValue ?: productNameField.emptyValue }) - this.productDescriptionField.addValueChangeListener({this.createProductViewModel.productDescription = it.value }) + this.productDescriptionField.addValueChangeListener({this.viewModel.productDescription = it.value }) - createProductViewModel.addPropertyChangeListener("productDescription", { + viewModel.addPropertyChangeListener("productDescription", { String newValue = it.newValue as String productDescriptionField.value = newValue ?: productDescriptionField.emptyValue }) - this.productUnitPriceField.addValueChangeListener({this.createProductViewModel.productUnitPrice = it.value}) + this.productUnitPriceField.addValueChangeListener({this.viewModel.productUnitPrice = it.value}) - createProductViewModel.addPropertyChangeListener("productUnitPrice", { + viewModel.addPropertyChangeListener("productUnitPrice", { String newValue = it.newValue as String productUnitPriceField.value = newValue ?: productUnitPriceField.emptyValue }) //bind combo boxes - createProductViewModel.addPropertyChangeListener("productUnit", { + viewModel.addPropertyChangeListener("productUnit", { ProductUnit newValue = it.newValue as ProductUnit if (newValue) { productUnitComboBox.value = newValue @@ -144,10 +147,10 @@ class CreateProductView extends HorizontalLayout{ } }) productUnitComboBox.addSelectionListener({ - createProductViewModel.setProductUnit(it.value as ProductUnit) + viewModel.setProductUnit(it.value as ProductUnit) }) - createProductViewModel.addPropertyChangeListener("productCategory", { + viewModel.addPropertyChangeListener("productCategory", { ProductCategory newValue = it.newValue as ProductCategory if (newValue) { productCategoryComboBox.value = newValue @@ -156,14 +159,14 @@ class CreateProductView extends HorizontalLayout{ } }) productCategoryComboBox.addSelectionListener({ - createProductViewModel.setProductCategory(it.value as ProductCategory) + viewModel.setProductCategory(it.value as ProductCategory) }) /* We listen to the valid properties. whenever the presenter resets values in the viewmodel and resets the valid properties the component error on the respective component is removed */ - createProductViewModel.addPropertyChangeListener({ + viewModel.addPropertyChangeListener({ switch (it.propertyName) { case "productNameValid": if (it.newValue || it.newValue == null) { @@ -210,51 +213,51 @@ class CreateProductView extends HorizontalLayout{ this.productNameField.addValueChangeListener({ event -> ValidationResult result = nameValidator.apply(event.getValue(), new ValueContext(this.productNameField)) if (result.isError()) { - createProductViewModel.productNameValid = false + viewModel.productNameValid = false UserError error = new UserError(result.getErrorMessage()) productNameField.setComponentError(error) } else { - createProductViewModel.productNameValid = true + viewModel.productNameValid = true } }) this.productDescriptionField.addValueChangeListener({ event -> ValidationResult result = nameValidator.apply(event.getValue(), new ValueContext(this.productDescriptionField)) if (result.isError()) { - createProductViewModel.productDescriptionValid = false + viewModel.productDescriptionValid = false UserError error = new UserError(result.getErrorMessage()) productDescriptionField.setComponentError(error) } else { - createProductViewModel.productDescriptionValid = true + viewModel.productDescriptionValid = true } }) this.productUnitPriceField.addValueChangeListener({ event -> ValidationResult result = numberValidator.apply(event.getValue(), new ValueContext(this.productUnitPriceField)) if (result.isError()) { - createProductViewModel.productUnitPriceValid = false + viewModel.productUnitPriceValid = false UserError error = new UserError(result.getErrorMessage()) productUnitPriceField.setComponentError(error) } else { - createProductViewModel.productUnitPriceValid = true + viewModel.productUnitPriceValid = true } }) this.productUnitComboBox.addSelectionListener({selection -> ValidationResult result = selectionValidator.apply(selection.getValue(), new ValueContext(this.productUnitComboBox)) if (result.isError()) { - createProductViewModel.productUnitValid = false + viewModel.productUnitValid = false UserError error = new UserError(result.getErrorMessage()) productUnitComboBox.setComponentError(error) } else { - createProductViewModel.productUnitValid = true + viewModel.productUnitValid = true } }) this.productCategoryComboBox.addSelectionListener({ selection -> ValidationResult result = selectionValidator.apply(selection.getValue(), new ValueContext(this.productCategoryComboBox)) if (result.isError()) { - createProductViewModel.productCategoryValid = false + viewModel.productCategoryValid = false UserError error = new UserError(result.getErrorMessage()) productCategoryComboBox.setComponentError(error) } else { - createProductViewModel.productCategoryValid = true + viewModel.productCategoryValid = true } }) } @@ -264,15 +267,20 @@ class CreateProductView extends HorizontalLayout{ * @return */ private boolean allValuesValid() { - return createProductViewModel.productNameValid \ - && createProductViewModel.productDescriptionValid \ - && createProductViewModel.productUnitValid \ - && createProductViewModel.productUnitPriceValid \ - && createProductViewModel.productCategoryValid + return viewModel.productNameValid \ + && viewModel.productDescriptionValid \ + && viewModel.productUnitValid \ + && viewModel.productUnitPriceValid \ + && viewModel.productCategoryValid } private void setupListeners(){ abortButton.addClickListener({ clearAllFields() }) + + createProductButton.addClickListener({ + controller.createNewProduct(viewModel.productCategory, viewModel.productDescription,viewModel.productName, Double.parseDouble(viewModel.productUnitPrice),viewModel.productUnit) + }) + } /** @@ -286,11 +294,11 @@ class CreateProductView extends HorizontalLayout{ productCategoryComboBox.selectedItem = productCategoryComboBox.clear() productUnitComboBox.selectedItem = productUnitComboBox.clear() - createProductViewModel.productNameValid = null - createProductViewModel.productDescriptionValid = null - createProductViewModel.productUnitPriceValid = null - createProductViewModel.productCategoryValid = null - createProductViewModel.productUnitValid = null + viewModel.productNameValid = null + viewModel.productDescriptionValid = null + viewModel.productUnitPriceValid = null + viewModel.productCategoryValid = null + viewModel.productUnitValid = null } } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy index ab0954178..29f1ffd4f 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy @@ -65,9 +65,10 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData private List fetchAllProductsFromDb() { List products = [] + String query = Queries.SELECT_ALL_PRODUCTS + "WHERE active = 1" provider.connect().withCloseable { - final PreparedStatement query = it.prepareStatement(Queries.SELECT_ALL_PRODUCTS) - final ResultSet resultSet = query.executeQuery() + final PreparedStatement statement = it.prepareStatement(query) + final ResultSet resultSet = statement.executeQuery() products.addAll(convertResultSet(resultSet)) } return products @@ -242,12 +243,12 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData @Override Optional fetch(ProductId productId) throws DatabaseQueryException { Connection connection = provider.connect() - String query = Queries.SELECT_ALL_PRODUCTS + " WHERE productId=?" + String query = Queries.SELECT_ALL_PRODUCTS + "WHERE active = 1 AND productId=?" Optional product = Optional.empty() connection.withCloseable { PreparedStatement preparedStatement = it.prepareStatement(query) - preparedStatement.setString(1, productId.identifier.toString()) + preparedStatement.setString(1, productId.toString()) ResultSet result = preparedStatement.executeQuery() while (result.next()) { @@ -299,7 +300,7 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData /** * Query for all available products. */ - final static String SELECT_ALL_PRODUCTS = "SELECT * FROM product" + final static String SELECT_ALL_PRODUCTS = "SELECT * FROM product " /** * Query for all items of an offer. diff --git a/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/products/ProductResourceServiceSpec.groovy b/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/products/ProductResourceServiceSpec.groovy new file mode 100644 index 000000000..a152fbcb1 --- /dev/null +++ b/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/products/ProductResourceServiceSpec.groovy @@ -0,0 +1,49 @@ +package life.qbic.portal.qoffer2.products + +import life.qbic.datamodel.dtos.business.services.PrimaryAnalysis +import life.qbic.datamodel.dtos.business.services.Product +import life.qbic.datamodel.dtos.business.services.ProductUnit +import life.qbic.datamodel.dtos.business.services.Sequencing +import life.qbic.portal.offermanager.dataresources.database.ConnectionProvider +import life.qbic.portal.offermanager.dataresources.products.ProductsDbConnector +import life.qbic.portal.offermanager.dataresources.products.ProductsResourcesService +import spock.lang.Specification + +import java.sql.Connection + +/** + *

Tests the functionality of the {@link life.qbic.portal.offermanager.dataresources.ResourcesService} of products {@link life.qbic.portal.offermanager.dataresources.products.ProductsResourcesService}

+ * + * @since 1.0.0 + * +*/ +class ProductResourceServiceSpec extends Specification{ + + + def "Products can be removed from the list"(){ + given: "a list of products" + Sequencing sequencing = new Sequencing("test product", "this is a test sequencing product", 0.5, ProductUnit.PER_GIGABYTE, "123") + PrimaryAnalysis primaryAnalysis = new PrimaryAnalysis("test product", "this is a test analysis product", 0.5, ProductUnit.PER_GIGABYTE, "123") + + PrimaryAnalysis primaryAnalysisCopy = new PrimaryAnalysis("test product", "this is a test analysis product", 0.5, ProductUnit.PER_GIGABYTE, "123") + + and: "the database session is mocked" + // the connection must only provide precompiled statements for the expected query template + Connection connection = Stub( Connection) + + //and: "a ConnectionProvider providing the stubbed connection" + ConnectionProvider connectionProvider = Stub (ConnectionProvider, {it.connect() >> connection}) + + and: "a resource service" + ProductsResourcesService resourcesService = new ProductsResourcesService(new ProductsDbConnector(connectionProvider)) + + when: "a product is removed" + resourcesService.addToResource(sequencing) + resourcesService.addToResource(primaryAnalysis) + resourcesService.removeFromResource(primaryAnalysisCopy) + + then: "the list does not longer contain the removed product" + resourcesService.iterator().toList().size() == 1 + resourcesService.iterator().toList().get(0) == sequencing + } +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy index f16f4682c..1d668e676 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy @@ -28,6 +28,7 @@ class CreateProduct implements CreateProductInput { @Override void create(Product product) { try { + //todo create new productId!!! the id is not provided from the controller. It must be generated in the use case itself dataSource.store(product) output.created(product) } catch(DatabaseQueryException databaseQueryException) { From e3b130e7eb935ad45e437d5696bf1b4e1d3febbf Mon Sep 17 00:00:00 2001 From: Sven F Date: Fri, 19 Mar 2021 13:40:46 +0100 Subject: [PATCH 07/32] Enable create project button if none exists yet (#421) --- .../components/offer/overview/OfferOverviewView.groovy | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy index b5e72fd60..07be28ba5 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy @@ -183,6 +183,14 @@ class OfferOverviewView extends FormLayout { }) } + private void checkProjectCreationAllowed(OfferOverview overview) { + if (overview.associatedProject.isPresent()) { + createProjectButton.setEnabled(false) + } else { + createProjectButton.setEnabled(true) + } + } + private void createResourceForDownload() { removeExistingResources() @@ -239,7 +247,7 @@ class OfferOverviewView extends FormLayout { overviewGrid.setEnabled(true) downloadBtn.setEnabled(true) updateOfferBtn.setEnabled(true) - createProjectButton.setEnabled(true) + checkProjectCreationAllowed(offerOverview) ui.setPollInterval(-1) }) } From e4c220ee13540c5b8c522a5d0a465bab538d4066 Mon Sep 17 00:00:00 2001 From: Sven F Date: Fri, 19 Mar 2021 13:41:29 +0100 Subject: [PATCH 08/32] Load last view after project creation (#418) After successful project creation, the last view is shown again. --- .../offer/overview/projectcreation/CreateProjectView.groovy | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectView.groovy index 82a65321a..6d329c091 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectView.groovy @@ -284,6 +284,12 @@ class CreateProjectView extends VerticalLayout{ new ProjectCode(model.resultingProjectCode))) } }) + this.model.addPropertyChangeListener("projectCreated", { + if (model.getStartedFromView().isPresent()) { + this.setVisible(false) + this.model.getStartedFromView().get().setVisible(true) + } + }) } private void bindData() { From ec28e531fcbabe00af12efa694cc9f0b7537405a Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Fri, 19 Mar 2021 13:51:17 +0100 Subject: [PATCH 09/32] Include proteomic and metabolomic category in productdbconnector --- .../dataresources/products/ProductsDbConnector.groovy | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy index 29f1ffd4f..b1da2d686 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy @@ -102,6 +102,12 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData case "Sequencing": productCategory = ProductCategory.SEQUENCING break + case "Proteomics": + productCategory = ProductCategory.PROTEOMIC + break + case "Metabolomics": + productCategory = ProductCategory.METABOLOMIC + break } if(!productCategory) { @@ -190,6 +196,8 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData if (product instanceof PrimaryAnalysis) return 'Primary Bioinformatics' if (product instanceof SecondaryAnalysis) return 'Secondary Bioinformatics' if (product instanceof DataStorage) return 'Data Storage' + if (product instanceof ProteomicAnalysis) return 'Proteomics' + if (product instanceof MetabolomicAnalysis) return 'Metabolomics' return null } From 41142f1be6ea5e1cedbb9e8f6b872ecdd2584f06 Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Fri, 19 Mar 2021 13:57:57 +0100 Subject: [PATCH 10/32] Update Pricing to account for Proteomic and metabolomic Products --- .../life/qbic/business/offers/Offer.groovy | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy index 5ea816498..55b43d351 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy @@ -236,8 +236,10 @@ class Offer { double getOverheadSum() { double overheadSum = 0 items.each { - // No overheads are assigned for data storage and project management - if (it.product instanceof PrimaryAnalysis || it.product instanceof SecondaryAnalysis || it.product instanceof Sequencing) { + if (it.product instanceof ProjectManagement || it.product instanceof DataStorage) { + // No overheads are assigned for data storage and project management + } + else { overheadSum += it.quantity * it.product.unitPrice * this.overhead } } @@ -269,7 +271,10 @@ class Offer { double getOverheadItemsNet() { double costOverheadItemsNet = 0 items.each { - if (it.product instanceof PrimaryAnalysis || it.product instanceof SecondaryAnalysis || it.product instanceof Sequencing) { + if (it.product instanceof ProjectManagement || it.product instanceof DataStorage) { + // No overheads are assigned for data storage and project management + } + else { costOverheadItemsNet += it.quantity * it.product.unitPrice } } @@ -298,7 +303,7 @@ class Offer { List getOverheadItems() { List listOverheadProductItem = [] items.each { - if (it.product instanceof PrimaryAnalysis || it.product instanceof SecondaryAnalysis || it.product instanceof Sequencing) { + if (it.product instanceof DataStorage || it.product instanceof ProjectManagement){ listOverheadProductItem.add(it) } } @@ -314,7 +319,10 @@ class Offer { List listNoOverheadProductItem = [] items.each { if (it.product instanceof DataStorage || it.product instanceof ProjectManagement) { - listNoOverheadProductItem.add(it) + // No overheads are assigned for data storage and project management + } + else { + listNoOverheadProductItem.add(it) } } return listNoOverheadProductItem From 027e9616c40f358a94e11ed9e58bd6dc93f9addf Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Fri, 19 Mar 2021 14:17:17 +0100 Subject: [PATCH 11/32] Fix Pricing --- .../src/main/groovy/life/qbic/business/offers/Offer.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy index 55b43d351..1a016f58a 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/offers/Offer.groovy @@ -304,6 +304,9 @@ class Offer { List listOverheadProductItem = [] items.each { if (it.product instanceof DataStorage || it.product instanceof ProjectManagement){ + // No overheads are assigned for data storage and project management + } + else { listOverheadProductItem.add(it) } } @@ -319,9 +322,6 @@ class Offer { List listNoOverheadProductItem = [] items.each { if (it.product instanceof DataStorage || it.product instanceof ProjectManagement) { - // No overheads are assigned for data storage and project management - } - else { listNoOverheadProductItem.add(it) } } From 55aaf468ce82c17732fafa88a69bc6bd231e9b1d Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Fri, 19 Mar 2021 14:24:33 +0100 Subject: [PATCH 12/32] Add Proteomic and metabolomic to createOfferViewModel --- .../components/offer/create/CreateOfferViewModel.groovy | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy index e5e359e5b..b61b5f6ab 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy @@ -32,6 +32,8 @@ class CreateOfferViewModel { List secondaryAnalysisProducts = new ObservableList(new ArrayList()) List managementProducts = new ObservableList(new ArrayList()) List storageProducts = new ObservableList(new ArrayList()) + List proteomicAnalysisProducts = new ObservableList(new ArrayList()) + List metabolomicAnalysisProducts = new ObservableList(new ArrayList()) ObservableList productItems = new ObservableList(new ArrayList()) ObservableList foundCustomers = new ObservableList(new ArrayList()) @@ -127,6 +129,12 @@ class CreateOfferViewModel { case DataStorage: storageProducts.add(productItem) break + case ProteomicAnalysis: + proteomicAnalysisProducts.add(productItem) + break + case MetabolomicAnalysis: + metabolomicAnalysisProducts.add(productItem) + break default: // this should not happen throw new RuntimeException("Unknown product category '${product.getClass().getSimpleName()}'") From 12909051854e04d7234e202c2ba066f6ab5bd2a5 Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Fri, 19 Mar 2021 15:02:46 +0100 Subject: [PATCH 13/32] Move ViewModel Changes to View PR --- .../components/offer/create/CreateOfferViewModel.groovy | 8 -------- 1 file changed, 8 deletions(-) diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy index b61b5f6ab..e5e359e5b 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy @@ -32,8 +32,6 @@ class CreateOfferViewModel { List secondaryAnalysisProducts = new ObservableList(new ArrayList()) List managementProducts = new ObservableList(new ArrayList()) List storageProducts = new ObservableList(new ArrayList()) - List proteomicAnalysisProducts = new ObservableList(new ArrayList()) - List metabolomicAnalysisProducts = new ObservableList(new ArrayList()) ObservableList productItems = new ObservableList(new ArrayList()) ObservableList foundCustomers = new ObservableList(new ArrayList()) @@ -129,12 +127,6 @@ class CreateOfferViewModel { case DataStorage: storageProducts.add(productItem) break - case ProteomicAnalysis: - proteomicAnalysisProducts.add(productItem) - break - case MetabolomicAnalysis: - metabolomicAnalysisProducts.add(productItem) - break default: // this should not happen throw new RuntimeException("Unknown product category '${product.getClass().getSimpleName()}'") From 1933bb2cb2b8d4334ffb195419be25692fc28c9b Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Mon, 22 Mar 2021 13:57:15 +0100 Subject: [PATCH 14/32] Introduces a fixed top percentage for notifications in stylesheet --- .../src/main/webapp/VAADIN/themes/mytheme/styles.css | 1 + 1 file changed, 1 insertion(+) diff --git a/offer-manager-app/src/main/webapp/VAADIN/themes/mytheme/styles.css b/offer-manager-app/src/main/webapp/VAADIN/themes/mytheme/styles.css index 1f0c06059..fdcec0b2e 100644 --- a/offer-manager-app/src/main/webapp/VAADIN/themes/mytheme/styles.css +++ b/offer-manager-app/src/main/webapp/VAADIN/themes/mytheme/styles.css @@ -14453,6 +14453,7 @@ div.v-layout.v-horizontal.v-widget { border-radius: 4px; text-align: center; position: fixed !important; + top: 50% !important; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -ms-backface-visibility: hidden; From a5c886d759f62446aebb80cf7574f1f2420b6e7d Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Mon, 22 Mar 2021 14:21:18 +0100 Subject: [PATCH 15/32] Update data-model-lib to stable version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4916c1686..2ffc6dce6 100644 --- a/pom.xml +++ b/pom.xml @@ -119,7 +119,7 @@ life.qbic data-model-lib - 2.4.0-SNAPSHOT + 2.4.0 com.vaadin From 0414b9a84e37e65987e857a6bd10262f2113364a Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Mon, 22 Mar 2021 14:33:52 +0100 Subject: [PATCH 16/32] Include PR in Changelog --- CHANGELOG.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d5f55b43c..087487e97 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,9 +17,10 @@ an existing project by its project identifier. * Finalized the ``life.qbic.business.products.archive.ArchiveProduct`` and ``life/qbic/business/products/create/CreateProduct.groovy`` use cases of the product maintenance and creation feature(`#411 `_). - **Fixed** +* Popup based Notifications are now properly centered in a liferay-environment(`#428 `_) + **Dependencies** **Deprecated** From dd0c6ce2c413ddebb103a59b3783d54b9f9e4560 Mon Sep 17 00:00:00 2001 From: Sven F Date: Mon, 22 Mar 2021 15:45:44 +0100 Subject: [PATCH 17/32] Refresh offer overview after project creation Co-authored-by: jnnfr --- CHANGELOG.rst | 3 +- .../offermanager/DependencyManager.groovy | 7 +++-- .../CreateProjectPresenter.groovy | 9 +++++- .../dataresources/offers/OfferOverview.groovy | 2 ++ .../offers/OverviewService.groovy | 30 ++++++++++++++++++- pom.xml | 2 +- 6 files changed, 47 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 087487e97..607d3cf1b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,9 +13,10 @@ This project adheres to `Semantic Versioning `_. * Link offers to project now. The ``life.qbic.business.offers.Offer`` and ``life.qbic.portal .offermanager.dataresources.offers`` have been extended with a new property to associate it with an existing project by its project identifier. - * Finalized the ``life.qbic.business.products.archive.ArchiveProduct`` and ``life/qbic/business/products/create/CreateProduct.groovy`` use cases of the product maintenance and creation feature(`#411 `_). +* After a project has been created from an offer, the offer overview is updated accordingly +(`#427 `_) **Fixed** diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy index 2217f619b..5ff3a2c5f 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy @@ -12,6 +12,7 @@ import life.qbic.business.persons.create.CreatePerson import life.qbic.business.offers.create.CreateOffer import life.qbic.datamodel.dtos.business.Offer import life.qbic.datamodel.dtos.general.Person +import life.qbic.datamodel.dtos.projectmanagement.Project import life.qbic.portal.offermanager.communication.EventEmitter import life.qbic.portal.offermanager.components.offer.overview.OfferOverviewController import life.qbic.portal.offermanager.components.offer.overview.OfferOverviewPresenter @@ -162,6 +163,7 @@ class DependencyManager { private ProjectSpaceResourceService projectSpaceResourceService private ProjectResourceService projectResourceService private EventEmitter personUpdateEvent + private EventEmitter projectCreatedEvent /** * Public constructor. @@ -225,7 +227,8 @@ class DependencyManager { private void setupServices() { this.offerService = new OfferResourcesService() - this.overviewService = new OverviewService(offerDbConnector, offerService) + this.projectCreatedEvent = new EventEmitter<>() + this.overviewService = new OverviewService(offerDbConnector, offerService, projectCreatedEvent) this.managerResourceService = new ProjectManagerResourceService(customerDbConnector) this.productsResourcesService = new ProductsResourcesService(productsDbConnector) this.affiliationService = new AffiliationResourcesService(customerDbConnector) @@ -428,7 +431,7 @@ class DependencyManager { log.error("Unexpected exception during ${MaintainProductsPresenter.getSimpleName()} setup", e) } try { - this.createProjectPresenter = new CreateProjectPresenter(createProjectModel, viewModel) + this.createProjectPresenter = new CreateProjectPresenter(createProjectModel, viewModel, projectCreatedEvent) } catch (Exception e) { log.error("Unexpected exception during ${CreateProjectPresenter.getSimpleName()} setup", e) } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectPresenter.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectPresenter.groovy index 748adcab3..061bf436b 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectPresenter.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectPresenter.groovy @@ -4,6 +4,7 @@ import life.qbic.business.projects.create.CreateProjectOutput import life.qbic.datamodel.dtos.business.OfferId import life.qbic.datamodel.dtos.projectmanagement.Project import life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier +import life.qbic.portal.offermanager.communication.EventEmitter import life.qbic.portal.offermanager.components.AppViewModel /** @@ -25,9 +26,14 @@ class CreateProjectPresenter implements CreateProjectOutput{ private final AppViewModel appViewModel - CreateProjectPresenter(CreateProjectViewModel createProjectViewModel, AppViewModel appViewModel) { + private final EventEmitter projectCreateEvent + + CreateProjectPresenter(CreateProjectViewModel createProjectViewModel, + AppViewModel appViewModel, + EventEmitter projectCreateEvent) { this.createProjectViewModel = createProjectViewModel this.appViewModel = appViewModel + this.projectCreateEvent = projectCreateEvent } /** @@ -46,6 +52,7 @@ class CreateProjectPresenter implements CreateProjectOutput{ void projectCreated(Project project) { this.createProjectViewModel.setProjectCreated(true) this.appViewModel.successNotifications.add("Project ${project.projectId} created.") + this.projectCreateEvent.emit(project) } /** diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferOverview.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferOverview.groovy index 5864f4047..051977c04 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferOverview.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferOverview.groovy @@ -1,5 +1,6 @@ package life.qbic.portal.offermanager.dataresources.offers +import groovy.transform.EqualsAndHashCode import life.qbic.datamodel.dtos.business.OfferId import life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier @@ -12,6 +13,7 @@ import life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier * * @since 1.0.0 */ +@EqualsAndHashCode class OfferOverview { final String projectTitle diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OverviewService.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OverviewService.groovy index ce9021f11..0e1bec3c1 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OverviewService.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OverviewService.groovy @@ -1,6 +1,7 @@ package life.qbic.portal.offermanager.dataresources.offers import life.qbic.datamodel.dtos.business.Offer +import life.qbic.datamodel.dtos.projectmanagement.Project import life.qbic.portal.offermanager.communication.EventEmitter import life.qbic.portal.offermanager.communication.Subscription import life.qbic.portal.offermanager.dataresources.ResourcesService @@ -24,13 +25,40 @@ class OverviewService implements ResourcesService { private final EventEmitter updatedOverviewEvent + private final EventEmitter projectCreatedEvent + OverviewService(OfferDbConnector offerDbConnector, - OfferResourcesService offerService) { + OfferResourcesService offerService, + EventEmitter projectCreatedEvent) { this.offerDbConnector = offerDbConnector this.updatedOverviewEvent = new EventEmitter<>() this.offerService = offerService + this.projectCreatedEvent = projectCreatedEvent this.offerOverviewList = offerDbConnector.loadOfferOverview() subscribeToNewOffers() + subscribeToNewProjects() + } + + private void subscribeToNewProjects() { + /* + Whenever a new project is created, we want to update the associated + offer overview with the project identifier detail + */ + projectCreatedEvent.register({ Project project -> + OfferOverview affectedOffer = offerOverviewList.find{ + it.offerId.equals(project.linkedOffer)} + if (affectedOffer) { + offerOverviewList.remove(affectedOffer) + OfferOverview updatedOverview = new OfferOverview( + affectedOffer.offerId, + affectedOffer.modificationDate, + affectedOffer.projectTitle, + affectedOffer.customer.toString(), + affectedOffer.totalPrice, + project.projectId) + this.addToResource(updatedOverview) + } + }) } private void subscribeToNewOffers(){ diff --git a/pom.xml b/pom.xml index 2ffc6dce6..5ee67667e 100644 --- a/pom.xml +++ b/pom.xml @@ -119,7 +119,7 @@ life.qbic data-model-lib - 2.4.0 + 2.5.0-SNAPSHOT com.vaadin From b7f3396ac94f0e030b8732655d34c491a1e1349f Mon Sep 17 00:00:00 2001 From: jnnfr Date: Tue, 23 Mar 2021 14:04:12 +0100 Subject: [PATCH 18/32] Implement copy product domain (#413) Co-authored-by: Sven F. Co-authored-by: Tobias Koch --- .../products/ProductsDbConnector.groovy | 9 +- offer-manager-domain/pom.xml | 2 +- .../qbic/business/products/Converter.groovy | 52 +++++-- .../qbic/business/products/Product.groovy | 127 ++++++++++++++++++ .../products/ProductDataSource.groovy | 39 ------ .../business/products/copy/CopyProduct.groovy | 105 +++++++++++++++ .../copy/CopyProductDataSource.groovy | 23 ++++ .../products/copy/CopyProductInput.groovy | 20 +++ .../products/copy/CopyProductOutput.groovy | 20 +++ .../products/create/CreateProduct.groovy | 7 +- .../create/CreateProductDataSource.groovy | 2 +- .../archive/ArchiveProductSpec.groovy | 4 +- .../products/copy/CopyProductSpec.groovy | 83 ++++++++++++ .../products/create/CreateProductSpec.groovy | 21 +-- 14 files changed, 444 insertions(+), 70 deletions(-) create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/Product.groovy delete mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/ProductDataSource.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProduct.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductDataSource.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductInput.groovy create mode 100644 offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductOutput.groovy create mode 100644 offer-manager-domain/src/test/groovy/life/qbic/business/products/copy/CopyProductSpec.groovy diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy index b1da2d686..2932da383 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy @@ -123,7 +123,6 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData } def createOfferItems(List items, int offerId) { - items.each {productItem -> String query = "INSERT INTO productitem (productId, quantity, offerid) "+ "VALUE(?,?,?)" @@ -174,13 +173,13 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData * Returns the product identifying running number given a productId * * @param productId String of productId stored in the DB e.g. "DS_1" - * @return identifier String of the iterative identifying part of the productId + * @return identifier Long of the iterative identifying part of the productId */ - static String parseProductId(String productId) { + static long parseProductId(String productId) throws NumberFormatException{ def splitId = productId.split("_") // The first entry [0] contains the product type which is assigned automatically, no need to parse it. String identifier = splitId[1] - return identifier + return Long.parseLong(identifier) } @@ -290,7 +289,7 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData } } - /** +/** * Class that encapsulates the available SQL queries. */ private static class Queries { diff --git a/offer-manager-domain/pom.xml b/offer-manager-domain/pom.xml index e06794614..71f11e211 100644 --- a/offer-manager-domain/pom.xml +++ b/offer-manager-domain/pom.xml @@ -23,7 +23,7 @@ log4j 1.2.17 -
+ diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy index 9157a4533..ae2b0ee02 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy @@ -33,7 +33,7 @@ class Converter { * @return */ static Product createProduct(ProductCategory category, String name, String description, double unitPrice, ProductUnit unit){ - String runningNumber = "0" + long runningNumber = 0 return createProductWithVersion(category,name,description,unitPrice,unit,runningNumber) } @@ -48,32 +48,64 @@ class Converter { * @param runningNumber The running version number of the product * @return */ - static Product createProductWithVersion(ProductCategory category, String name, String description, double unitPrice, ProductUnit unit, String runningNumber){ + static Product createProductWithVersion(ProductCategory category, String name, String description, double unitPrice, ProductUnit unit, long runningNumber){ Product product switch (category) { case "DATA_STORAGE": - product = new DataStorage(name, description, unitPrice,unit, runningNumber) + product = new DataStorage(name, description, unitPrice,unit, runningNumber.toString()) break case "PRIMARY_BIOINFO": - product = new PrimaryAnalysis(name, description, unitPrice,unit, runningNumber) + product = new PrimaryAnalysis(name, description, unitPrice,unit, runningNumber.toString()) break case "PROJECT_MANAGEMENT": - product = new ProjectManagement(name, description, unitPrice,unit, runningNumber) + product = new ProjectManagement(name, description, unitPrice,unit, runningNumber.toString()) break case "SECONDARY_BIOINFO": - product = new SecondaryAnalysis(name, description, unitPrice,unit, runningNumber) + product = new SecondaryAnalysis(name, description, unitPrice,unit, runningNumber.toString()) break case "SEQUENCING": - product = new Sequencing(name, description, unitPrice,unit, runningNumber) + product = new Sequencing(name, description, unitPrice,unit, runningNumber.toString()) break case "PROTEOMIC": - product = new ProteomicAnalysis(name, description, unitPrice,unit, runningNumber) + product = new ProteomicAnalysis(name, description, unitPrice,unit, runningNumber.toString()) break case "METABOLOMIC": - product = new MetabolomicAnalysis(name, description, unitPrice,unit, runningNumber) + product = new MetabolomicAnalysis(name, description, unitPrice,unit, runningNumber.toString()) break } - if(!product) throw new IllegalArgumentException("Cannot parse products") + if(!product) throw new IllegalArgumentException("Cannot parse product") return product } + + /** + * Retrieves the category of the given product + * @param product The product of a specific product category + * @return the product category of the given product + */ + static ProductCategory getCategory(Product product){ + if(product instanceof ProjectManagement) return ProductCategory.PROJECT_MANAGEMENT + if(product instanceof Sequencing) return ProductCategory.SEQUENCING + if(product instanceof PrimaryAnalysis) return ProductCategory.PRIMARY_BIOINFO + if(product instanceof SecondaryAnalysis) return ProductCategory.SECONDARY_BIOINFO + if(product instanceof DataStorage) return ProductCategory.DATA_STORAGE + if(product instanceof ProteomicAnalysis) return ProductCategory.PROTEOMIC + if(product instanceof MetabolomicAnalysis) return ProductCategory.METABOLOMIC + + throw new IllegalArgumentException("Cannot parse category of the provided product ${product.toString()}") + } + + static life.qbic.business.products.Product convertDTOtoProduct(Product product){ + ProductCategory category = getCategory(product) + return new life.qbic.business.products.Product.Builder(category, + product.productName, + product.description, + product.unitPrice, + product.unit) + .build() + + } + + static Product convertProductToDTO(life.qbic.business.products.Product product){ + return createProductWithVersion(product.category,product.name, product.description, product.unitPrice, product.unit, product.id.uniqueId) + } } diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/Product.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/Product.groovy new file mode 100644 index 000000000..5d1ef13d7 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/Product.groovy @@ -0,0 +1,127 @@ +package life.qbic.business.products + +import life.qbic.datamodel.dtos.business.ProductCategory +import life.qbic.datamodel.dtos.business.ProductId +import life.qbic.datamodel.dtos.business.services.ProductUnit + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +/** + *

Represents the product business model

+ *
+ *

This class should be used in the business context of product creation

+ * + * @since 1.0.0 + * +*/ +class Product { + private ProductCategory category + private String name + private String description + private double unitPrice + private ProductUnit unit + private ProductId id + + static class Builder{ + ProductCategory category + String name + String description + double unitPrice + ProductUnit unit + ProductId id + + Builder(ProductCategory category, String name, String description, double unitPrice, ProductUnit unit){ + this.category = Objects.requireNonNull(category) + this.name = Objects.requireNonNull(name) + this.description = Objects.requireNonNull(description) + this.unitPrice = Objects.requireNonNull(unitPrice) + this.unit = Objects.requireNonNull(unit) + } + + Builder id(ProductId id){ + this.id = id + return this + } + + Product build(){ + return new Product(this) + } + } + + Product(Builder builder){ + this.category = builder.category + this.name = builder.name + this.description = builder.description + this.unitPrice = builder.unitPrice + this.unit = builder.unit + } + + /** + * Calculates the SHA checksum for the product + * The checksum is computed based on the product name, description, unit, unit price and the category + * + * @return a string containing the checksum for this product + */ + String checksum(){ + MessageDigest digest = MessageDigest.getInstance("SHA-256") + return getProductChecksum(digest,this) + } + + /** + * Compute the checksum for a product based on the encryption method provided + * + * @param digest The digestor will digest the message that needs to be encrypted + * @param product Contains the product information + * @return a string that encrypts the product object + */ + private static String getProductChecksum(MessageDigest digest, Product product) + { + //digest crucial offer characteristics + digest.update(product.name.getBytes(StandardCharsets.UTF_8)) + + digest.update(product.description.getBytes(StandardCharsets.UTF_8)) + + digest.update(product.unit.value.getBytes(StandardCharsets.UTF_8)) + digest.update(product.unitPrice.toString().getBytes(StandardCharsets.UTF_8)) + digest.update(product.category.toString().getBytes(StandardCharsets.UTF_8)) + + //Get the hash's bytes + byte[] bytes = digest.digest() + + //This bytes[] has bytes in decimal format + //Convert it to hexadecimal format + StringBuilder sb = new StringBuilder() + for(int i=0; i< bytes.length ;i++) + { + sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1)); + } + + //return complete hash + return sb.toString() + } + + ProductCategory getCategory() { + return category + } + + String getName() { + return name + } + + String getDescription() { + return description + } + + double getUnitPrice() { + return unitPrice + } + + ProductUnit getUnit() { + return unit + } + + ProductId getId() { + return id + } +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/ProductDataSource.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/ProductDataSource.groovy deleted file mode 100644 index b0d47ee73..000000000 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/products/ProductDataSource.groovy +++ /dev/null @@ -1,39 +0,0 @@ -package life.qbic.business.products - -import life.qbic.business.exceptions.DatabaseQueryException -import life.qbic.datamodel.dtos.business.ProductId -import life.qbic.datamodel.dtos.business.services.Product - -/** - * Defines the methods of the Datasource implementation - * - * @since: 1.0.0 - * - */ -interface ProductDataSource { - - /** - * Fetches a product from the database - * @param productId The product id of the product to be fetched - * @return returns an optional that contains the product if it has been found - * @since 1.0.0 - * @throws DatabaseQueryException - */ - Optional fetch(ProductId productId) throws DatabaseQueryException - - /** - * Stores a product in the database - * @param product The product that needs to be stored - * @since 1.0.0 - * @throws DatabaseQueryException - */ - void store(Product product) throws DatabaseQueryException - - /** - * A product is archived by setting it inactive - * @param product The product that needs to be archived - * @since 1.0.0 - * @throws DatabaseQueryException - */ - void archive(Product product) throws DatabaseQueryException -} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProduct.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProduct.groovy new file mode 100644 index 000000000..41093147e --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProduct.groovy @@ -0,0 +1,105 @@ +package life.qbic.business.products.copy + +import life.qbic.business.Constants +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.business.logging.Logger +import life.qbic.business.logging.Logging +import life.qbic.business.products.Converter +import life.qbic.business.products.create.CreateProduct +import life.qbic.business.products.create.CreateProductDataSource +import life.qbic.business.products.create.CreateProductInput +import life.qbic.business.products.create.CreateProductOutput +import life.qbic.datamodel.dtos.business.ProductId +import life.qbic.datamodel.dtos.business.services.Product + +/** + *

4.3.2 Copy Service Product

+ *
+ *

Offer Administrators are allowed to create a new permutation of an existing product. + *
New permutations can include changes in unit price, sequencing technology and other attributes of service products. + *

+ * + * @since: 1.0.0 + * + */ +class CopyProduct implements CopyProductInput, CreateProductOutput { + + private static final Logging log = Logger.getLogger(this.class) + + private final CopyProductDataSource dataSource + private final CopyProductOutput output + private final CreateProductInput createProduct + + /** + * The only constructor for this use case + * @param dataSource - a data source that provides mandatory functionality + * @param output - an output that provides mandatory functionality + * @param createProductInput - a CreateProduct use case that is used to create the product + */ + CopyProduct(CopyProductDataSource dataSource, CopyProductOutput output, CreateProductDataSource createProductDataSource) { + this.dataSource = dataSource + this.output = output + this.createProduct = new CreateProduct(createProductDataSource, this) + } + + /** + * {@inheritDoc} + */ + @Override + void copyModified(Product product) { + try { + //1. retrieve product from db + Product existingProduct = getExistingProduct(product.productId) + //2. compare if there is a difference between the products in order + if (theProductHasChanged(product,existingProduct)) { + //3. call the CreateProduct use case (new id is created here) + createProduct.create(product) + } else { + foundDuplicate(product) + } + } catch (DatabaseQueryException databaseQueryException) { + log.error("The copied product ${product.productId.toString()} cannot be found in the database", databaseQueryException) + output.failNotification("The copied product ${product.productId.toString()} cannot be found in the database") + } catch(Exception ignore){ + //there is no product present, this should not happen + log.error("An unexpected during the project creation occurred.", ignore) + output.failNotification("An unexpected during the project creation occurred. " + + "Please contact ${Constants.QBIC_HELPDESK_EMAIL}.") + } + } + + private Product getExistingProduct(ProductId productId){ + return dataSource.fetch(productId).get() + } + + private static boolean theProductHasChanged(Product product1, Product product2){ + life.qbic.business.products.Product copiedProduct = Converter.convertDTOtoProduct(product1) + life.qbic.business.products.Product oldProduct = Converter.convertDTOtoProduct(product2) + + copiedProduct.checksum() != oldProduct.checksum() + } + + /** + * {@inheritDoc} + */ + @Override + void failNotification(String notification) { + output.failNotification(notification) + } + + /** + * {@inheritDoc} + */ + @Override + void created(Product product) { + output.copied(product) + } + + /** + * {@inhertDoc} + */ + @Override + void foundDuplicate(Product product) { + output.failNotification("A product with the same content like ${product.productName} already exists.") + } +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductDataSource.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductDataSource.groovy new file mode 100644 index 000000000..68a320c18 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductDataSource.groovy @@ -0,0 +1,23 @@ +package life.qbic.business.products.copy + +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.business.ProductId +import life.qbic.datamodel.dtos.business.services.Product + +/** + *

Data source for the {@link life.qbic.business.products.copy.CopyProduct} use case

+ * + * @since 1.0.0 + */ +interface CopyProductDataSource { + + /** + * Fetches a product from the database + * @param productId The product id of the product to be fetched + * @return returns an optional that contains the product if it has been found + * @since 1.0.0 + * @throws life.qbic.business.exceptions.DatabaseQueryException + */ + Optional fetch(ProductId productId) throws DatabaseQueryException + +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductInput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductInput.groovy new file mode 100644 index 000000000..6abf69bcb --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductInput.groovy @@ -0,0 +1,20 @@ +package life.qbic.business.products.copy + +import life.qbic.datamodel.dtos.business.ProductId +import life.qbic.datamodel.dtos.business.services.Product + +/** + * Input interface for the {@link CopyProduct} use case + * + * @since: 1.0.0 + * + */ +interface CopyProductInput { + + /** + * Creates a product and populates it with provided information + * @param product The modified product information. The identifier should already be present. + * @since 1.0.0 + */ + void copyModified(Product product) +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductOutput.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductOutput.groovy new file mode 100644 index 000000000..3428c9b62 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/copy/CopyProductOutput.groovy @@ -0,0 +1,20 @@ +package life.qbic.business.products.copy + +import life.qbic.business.UseCaseFailure +import life.qbic.datamodel.dtos.business.services.Product + +/** + * Output interface for the {@link CopyProduct} use case + * + * @since: 1.0.0 + * + */ +interface CopyProductOutput extends UseCaseFailure { + + /** + * A copy of a product has been created. This method is called after the copied product has been stored in the database. + * @param product The product that has been copied + * @since 1.0.0 + */ + void copied(Product product) +} diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy index 1d668e676..c93a38e3e 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy @@ -3,6 +3,9 @@ package life.qbic.business.products.create import life.qbic.business.exceptions.DatabaseQueryException import life.qbic.business.logging.Logger import life.qbic.business.logging.Logging +import life.qbic.business.products.Converter +import life.qbic.datamodel.dtos.business.ProductCategory +import life.qbic.datamodel.dtos.business.ProductId import life.qbic.datamodel.dtos.business.services.Product /** @@ -28,15 +31,15 @@ class CreateProduct implements CreateProductInput { @Override void create(Product product) { try { - //todo create new productId!!! the id is not provided from the controller. It must be generated in the use case itself dataSource.store(product) output.created(product) } catch(DatabaseQueryException databaseQueryException) { log.error("Product creation failed", databaseQueryException) output.failNotification("Could not create product $product.productName with id $product.productId") } catch(ProductExistsException productExistsException) { - log.warn("Product \"$product.productName\" already existed.", productExistsException) + log.warn("Product \"$product.productName\" already exists.", productExistsException) output.foundDuplicate(product) } } + } diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductDataSource.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductDataSource.groovy index 6ce0fa435..5818e1bde 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductDataSource.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductDataSource.groovy @@ -1,7 +1,6 @@ package life.qbic.business.products.create import life.qbic.business.exceptions.DatabaseQueryException -import life.qbic.datamodel.dtos.business.ProductId import life.qbic.datamodel.dtos.business.services.Product /** @@ -19,4 +18,5 @@ interface CreateProductDataSource { * @throws ProductExistsException if the product already exists in the data source */ void store(Product product) throws DatabaseQueryException, ProductExistsException + } \ No newline at end of file diff --git a/offer-manager-domain/src/test/groovy/life/qbic/business/products/archive/ArchiveProductSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/business/products/archive/ArchiveProductSpec.groovy index ea520a533..203ff7e30 100644 --- a/offer-manager-domain/src/test/groovy/life/qbic/business/products/archive/ArchiveProductSpec.groovy +++ b/offer-manager-domain/src/test/groovy/life/qbic/business/products/archive/ArchiveProductSpec.groovy @@ -7,9 +7,9 @@ import life.qbic.datamodel.dtos.business.services.ProductUnit import spock.lang.Specification /** - *

Archive Product tests

+ *

{@kink ArchiveProduct} tests

* - *

This Specification contains tests for the use ArchiveProduct use case

+ *

This Specification contains tests for the {@link ArchiveProduct} use case

* * @since 1.0.0 */ diff --git a/offer-manager-domain/src/test/groovy/life/qbic/business/products/copy/CopyProductSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/business/products/copy/CopyProductSpec.groovy new file mode 100644 index 000000000..a6b6491ae --- /dev/null +++ b/offer-manager-domain/src/test/groovy/life/qbic/business/products/copy/CopyProductSpec.groovy @@ -0,0 +1,83 @@ +package life.qbic.business.products.copy + +import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.business.products.create.CreateProduct +import life.qbic.business.products.create.CreateProductDataSource +import life.qbic.business.products.create.CreateProductInput +import life.qbic.business.products.create.ProductExistsException +import life.qbic.datamodel.dtos.business.ProductCategory +import life.qbic.datamodel.dtos.business.ProductId +import life.qbic.datamodel.dtos.business.services.AtomicProduct +import life.qbic.datamodel.dtos.business.services.Product +import life.qbic.datamodel.dtos.business.services.ProductUnit +import life.qbic.datamodel.dtos.business.services.Sequencing +import org.apache.tools.ant.taskdefs.Copy +import spock.lang.IgnoreRest +import spock.lang.Shared +import spock.lang.Specification + +/** + *

{@link CopyProduct} tests

+ * + *

This Specification contains tests for the {@link CopyProduct} use case

+ * + * @since 1.0.0 + */ +class CopyProductSpec extends Specification { + + + CreateProductDataSource createProductDataSource = Stub(CreateProductDataSource) + CopyProductDataSource dataSource = Stub(CopyProductDataSource) + CopyProductOutput output = Mock(CopyProductOutput) + Product product = new Sequencing("test product", "this is a test product", 0.5, + ProductUnit.PER_GIGABYTE,"1") + + def "FailNotification forwards received messages to the output"() { + given: "a CreateProductDataSource that throws a DatabaseQueryException" + createProductDataSource.store(product) >> {throw new DatabaseQueryException("Test exception")} + and: "a copy use case with this datasource" + CopyProduct copyProduct = new CopyProduct(dataSource, output, createProductDataSource) + + when: "copyModified is called" + copyProduct.copyModified(product) + + then: "a fail notification is received in the output" + 1 * output.failNotification(_ as String) + and: "no output is registered" + 0 * output.copied(_) + noExceptionThrown() + } + + def "A duplicated entry leads to fail notification"() { + given: "a CreateProductDataSource that throws a ProductExistsException" + createProductDataSource.store(product) >> {throw new ProductExistsException(product.getProductId(), "Test exception")} + and: "a copy use case with this datasource" + CopyProduct copyProduct = new CopyProduct(dataSource, output, createProductDataSource) + + when: "copyModified is called" + copyProduct.copyModified(product) + + then: "a fail notification is received in the output" + 1 * output.failNotification(_ as String) + and: "no other output is registered" + 0 * output.copied(_) + noExceptionThrown() + } + + def "CopyModified rejects non existent products"() { + given: "A product that is not in the database" + dataSource.fetch(product.getProductId()) >> Optional.empty() + and: "a copy use case" + CopyProduct copyProduct = new CopyProduct(dataSource, output, createProductDataSource) + + when: "copyModified is called with the unknown product" + copyProduct.copyModified(product) + + then: "fail notification is created" + 1 * output.failNotification(_ as String) + and: "no other output is registered" + 0 * output.copied(_) + noExceptionThrown() + } + +} diff --git a/offer-manager-domain/src/test/groovy/life/qbic/business/products/create/CreateProductSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/business/products/create/CreateProductSpec.groovy index 10acb195e..77ce5d9bc 100644 --- a/offer-manager-domain/src/test/groovy/life/qbic/business/products/create/CreateProductSpec.groovy +++ b/offer-manager-domain/src/test/groovy/life/qbic/business/products/create/CreateProductSpec.groovy @@ -1,10 +1,11 @@ package life.qbic.business.products.create import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.business.ProductCategory import life.qbic.datamodel.dtos.business.ProductId -import life.qbic.datamodel.dtos.business.services.AtomicProduct import life.qbic.datamodel.dtos.business.services.Product import life.qbic.datamodel.dtos.business.services.ProductUnit +import life.qbic.datamodel.dtos.business.services.Sequencing import spock.lang.Specification /** @@ -15,21 +16,17 @@ import spock.lang.Specification * @since 1.0.0 */ class CreateProductSpec extends Specification { - CreateProductOutput output - ProductId productId - Product product + CreateProductOutput output = Mock(CreateProductOutput) + ProductId productId = new ProductId("SE","1") + Product product = new Sequencing("test product", "this is a test product", 0.5, ProductUnit.PER_GIGABYTE, "1") //todo use long when ProductId builder is fixed - def setup() { - output = Mock(CreateProductOutput) - productId = new ProductId("Test", "1234") - product = new AtomicProduct("test product", "this is a test product", 0.5, ProductUnit.PER_GIGABYTE, productId) - } def "Create stores the provided product in the data source"() { given: "a data source that stores a product" CreateProductDataSource dataSource = Stub(CreateProductDataSource) String dataStatus = "" dataSource.store(product) >> { dataStatus = "stored" } + dataSource.fetchLatestProductIdentifierVersion(ProductCategory.SEQUENCING) >> Optional.of(productId) and: "an instance of the use case" CreateProduct createProduct = new CreateProduct(dataSource, output) @@ -50,9 +47,10 @@ class CreateProductSpec extends Specification { String dataStatus = "" dataSource.store(product) >> { dataStatus = "not stored" - println(dataStatus) throw new ProductExistsException(productId) } + dataSource.fetchLatestProductIdentifierVersion(ProductCategory.SEQUENCING) >> Optional.of(productId) + and: "an instance of the use case" CreateProduct createProduct = new CreateProduct(dataSource, output) @@ -74,6 +72,8 @@ class CreateProductSpec extends Specification { dataSource.store(product) >> { dataStatus = "not stored" throw new DatabaseQueryException("This is a test") } + dataSource.fetchLatestProductIdentifierVersion(ProductCategory.SEQUENCING) >> Optional.of(productId) + and: "an instance of the use case" CreateProduct createProduct = new CreateProduct(dataSource, output) @@ -87,4 +87,5 @@ class CreateProductSpec extends Specification { and: "the data was stored" dataStatus == "not stored" } + } From 99c2ec59291f84907e06261436d91f162fd1801b Mon Sep 17 00:00:00 2001 From: jnnfr Date: Thu, 25 Mar 2021 12:57:52 +0100 Subject: [PATCH 19/32] Add product id to created products (#431) This PR implements the product id creation in the database before a product is created. Co-authored-by: Tobias Koch --- .../products/ProductsDbConnector.groovy | 47 +++++++++++++++---- .../webapp/VAADIN/themes/mytheme/styles.css | 1 - .../qbic/business/products/Converter.groovy | 2 +- .../products/create/CreateProduct.groovy | 18 +++++-- .../create/CreateProductDataSource.groovy | 6 ++- .../products/create/CreateProductSpec.groovy | 35 +++++++++----- 6 files changed, 81 insertions(+), 28 deletions(-) diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy index 2932da383..f57f6fdca 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy @@ -266,16 +266,15 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData } /** - * Stores a product in the database - * @param product The product that needs to be stored - * @since 1.0.0 - * @throws DatabaseQueryException if any technical interaction with the data source fails - * @throws ProductExistsException if the product already exists in the data source + * + * {@inheritDoc} */ @Override - void store(Product product) throws DatabaseQueryException, ProductExistsException { + ProductId store(Product product) throws DatabaseQueryException, ProductExistsException { Connection connection = provider.connect() + ProductId productId = createProductId(product) + connection.withCloseable { PreparedStatement preparedStatement = it.prepareStatement(Queries.INSERT_PRODUCT) preparedStatement.setString(1, getProductType(product)) @@ -283,13 +282,45 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData preparedStatement.setString(3, product.productName) preparedStatement.setDouble(4, product.unitPrice) preparedStatement.setString(5, product.unit.value) - preparedStatement.setString(6, product.productId.toString()) + preparedStatement.setString(6, productId.toString()) preparedStatement.execute() } + + return productId } -/** + private ProductId createProductId(Product product){ + String productType = product.productId.type + String version = fetchLatestIdentifier(productType) //todo exchange with long + + return new ProductId(productType,version) + } + + private Long fetchLatestIdentifier(String productType){ + String query = "SELECT MAX(productId) FROM product WHERE productId LIKE ?" + Connection connection = provider.connect() + + String category = productType + "_%" + Long latestUniqueId = 0 + + connection.withCloseable { + PreparedStatement preparedStatement = it.prepareStatement(query) + preparedStatement.setString(1, category) + + ResultSet result = preparedStatement.executeQuery() + + while(result.next()){ + String id = result.getString(1) + + latestUniqueId = Long.parseLong(id.split('_')[1]) + } + } + + return latestUniqueId + 1 + } + + /** * Class that encapsulates the available SQL queries. */ private static class Queries { diff --git a/offer-manager-app/src/main/webapp/VAADIN/themes/mytheme/styles.css b/offer-manager-app/src/main/webapp/VAADIN/themes/mytheme/styles.css index fdcec0b2e..1f0c06059 100644 --- a/offer-manager-app/src/main/webapp/VAADIN/themes/mytheme/styles.css +++ b/offer-manager-app/src/main/webapp/VAADIN/themes/mytheme/styles.css @@ -14453,7 +14453,6 @@ div.v-layout.v-horizontal.v-widget { border-radius: 4px; text-align: center; position: fixed !important; - top: 50% !important; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -ms-backface-visibility: hidden; diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy index ae2b0ee02..b001ae2af 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy @@ -33,7 +33,7 @@ class Converter { * @return */ static Product createProduct(ProductCategory category, String name, String description, double unitPrice, ProductUnit unit){ - long runningNumber = 0 + long runningNumber = 0 //todo it should be possible to create products without a running number return createProductWithVersion(category,name,description,unitPrice,unit,runningNumber) } diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy index c93a38e3e..d61b60bc9 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProduct.groovy @@ -1,5 +1,6 @@ package life.qbic.business.products.create +import life.qbic.business.Constants import life.qbic.business.exceptions.DatabaseQueryException import life.qbic.business.logging.Logger import life.qbic.business.logging.Logging @@ -15,7 +16,6 @@ import life.qbic.datamodel.dtos.business.services.Product *

* * @since: 1.0.0 - * */ class CreateProduct implements CreateProductInput { @@ -31,14 +31,22 @@ class CreateProduct implements CreateProductInput { @Override void create(Product product) { try { - dataSource.store(product) - output.created(product) + ProductId createdProductId = dataSource.store(product) + //create product with new product ID + ProductCategory category = Converter.getCategory(product) + Product storedProduct = Converter.createProductWithVersion(category,product.productName,product.description,product.unitPrice, product.unit, createdProductId.uniqueId) + + output.created(storedProduct) } catch(DatabaseQueryException databaseQueryException) { log.error("Product creation failed", databaseQueryException) - output.failNotification("Could not create product $product.productName with id $product.productId") + output.failNotification("Could not create new product $product.productName") } catch(ProductExistsException productExistsException) { - log.warn("Product \"$product.productName\" already exists.", productExistsException) + log.warn("Product $product.productName with identifier $product.productId already exists.", productExistsException) output.foundDuplicate(product) + } catch(Exception exception) { + log.error("An unexpected during the project creation occurred.", exception) + output.failNotification("An unexpected during the project creation occurred. " + + "Please contact ${Constants.QBIC_HELPDESK_EMAIL}.") } } diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductDataSource.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductDataSource.groovy index 5818e1bde..bc6fbfa8f 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductDataSource.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/create/CreateProductDataSource.groovy @@ -1,6 +1,7 @@ package life.qbic.business.products.create import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.dtos.business.ProductId import life.qbic.datamodel.dtos.business.services.Product /** @@ -13,10 +14,11 @@ interface CreateProductDataSource { /** * Stores a product in the database * @param product The product that needs to be stored + * @return The product identifier of the stored product * @since 1.0.0 * @throws DatabaseQueryException if any technical interaction with the data source fails * @throws ProductExistsException if the product already exists in the data source */ - void store(Product product) throws DatabaseQueryException, ProductExistsException + ProductId store(Product product) throws DatabaseQueryException, ProductExistsException -} \ No newline at end of file +} diff --git a/offer-manager-domain/src/test/groovy/life/qbic/business/products/create/CreateProductSpec.groovy b/offer-manager-domain/src/test/groovy/life/qbic/business/products/create/CreateProductSpec.groovy index 77ce5d9bc..e52ba6e39 100644 --- a/offer-manager-domain/src/test/groovy/life/qbic/business/products/create/CreateProductSpec.groovy +++ b/offer-manager-domain/src/test/groovy/life/qbic/business/products/create/CreateProductSpec.groovy @@ -1,7 +1,6 @@ package life.qbic.business.products.create import life.qbic.business.exceptions.DatabaseQueryException -import life.qbic.datamodel.dtos.business.ProductCategory import life.qbic.datamodel.dtos.business.ProductId import life.qbic.datamodel.dtos.business.services.Product import life.qbic.datamodel.dtos.business.services.ProductUnit @@ -17,16 +16,15 @@ import spock.lang.Specification */ class CreateProductSpec extends Specification { CreateProductOutput output = Mock(CreateProductOutput) - ProductId productId = new ProductId("SE","1") + ProductId createdProductId = new ProductId("SE","2") Product product = new Sequencing("test product", "this is a test product", 0.5, ProductUnit.PER_GIGABYTE, "1") //todo use long when ProductId builder is fixed def "Create stores the provided product in the data source"() { given: "a data source that stores a product" CreateProductDataSource dataSource = Stub(CreateProductDataSource) - String dataStatus = "" - dataSource.store(product) >> { dataStatus = "stored" } - dataSource.fetchLatestProductIdentifierVersion(ProductCategory.SEQUENCING) >> Optional.of(productId) + dataSource.store(product) >> { createdProductId } + and: "an instance of the use case" CreateProduct createProduct = new CreateProduct(dataSource, output) @@ -34,11 +32,28 @@ class CreateProductSpec extends Specification { createProduct.create(product) then: "the output is informed and no failure notification is send" - 1 * output.created(product) + 1 * output.created({Product product1 -> + product1.productId == createdProductId + }) 0 * output.foundDuplicate(_) 0 * output.failNotification(_) - and: "the data was stored in the database" - dataStatus == "stored" + } + + def "Create sends a failure notification if the datasource returns null"() { + given: "a data source that stores a product" + CreateProductDataSource dataSource = Stub(CreateProductDataSource) + dataSource.store(product) >> { null } + + and: "an instance of the use case" + CreateProduct createProduct = new CreateProduct(dataSource, output) + + when: "the create method is called" + createProduct.create(product) + + then: "the output is informed and no failure notification is send" + 0 * output.created(_) + 0 * output.foundDuplicate(_) + 1 * output.failNotification(_) } def "Create informs the output that an entry matching the provided product already exists"() { @@ -47,9 +62,8 @@ class CreateProductSpec extends Specification { String dataStatus = "" dataSource.store(product) >> { dataStatus = "not stored" - throw new ProductExistsException(productId) + throw new ProductExistsException(createdProductId) } - dataSource.fetchLatestProductIdentifierVersion(ProductCategory.SEQUENCING) >> Optional.of(productId) and: "an instance of the use case" CreateProduct createProduct = new CreateProduct(dataSource, output) @@ -72,7 +86,6 @@ class CreateProductSpec extends Specification { dataSource.store(product) >> { dataStatus = "not stored" throw new DatabaseQueryException("This is a test") } - dataSource.fetchLatestProductIdentifierVersion(ProductCategory.SEQUENCING) >> Optional.of(productId) and: "an instance of the use case" CreateProduct createProduct = new CreateProduct(dataSource, output) From f1a60d51f0233dfc6517337239765747ba84c5ab Mon Sep 17 00:00:00 2001 From: jnnfr Date: Fri, 26 Mar 2021 11:12:15 +0100 Subject: [PATCH 20/32] Refresh search person after update (#436) This updates the updated person's information also for the view. Furthermore, the UpdatePersonView was introduced to allow better separation Co-authored-by: Sven Fillinger --- CHANGELOG.rst | 2 + .../offermanager/DependencyManager.groovy | 5 +- .../person/create/CreatePersonView.groovy | 21 +++--- .../person/search/SearchPersonView.groovy | 1 + .../person/update/UpdatePersonView.groovy | 64 +++++++++++++++++++ .../update/UpdatePersonViewModel.groovy | 10 +-- 6 files changed, 85 insertions(+), 18 deletions(-) create mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/update/UpdatePersonView.groovy diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 607d3cf1b..6fe6b50eb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,10 +17,12 @@ an existing project by its project identifier. use cases of the product maintenance and creation feature(`#411 `_). * After a project has been created from an offer, the offer overview is updated accordingly (`#427 `_) +* Add the UpdatePersonView to separate the Update and Create Person use cases more consequently (`#436 `_) **Fixed** * Popup based Notifications are now properly centered in a liferay-environment(`#428 `_) +* Properly refresh the SearchPersonView after Updating a Person (`#436 `_) **Dependencies** diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy index 5ff3a2c5f..885ca6505 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy @@ -22,6 +22,7 @@ import life.qbic.portal.offermanager.components.offer.overview.projectcreation.C import life.qbic.portal.offermanager.components.offer.overview.projectcreation.CreateProjectViewModel import life.qbic.portal.offermanager.components.person.search.SearchPersonView import life.qbic.portal.offermanager.components.person.search.SearchPersonViewModel +import life.qbic.portal.offermanager.components.person.update.UpdatePersonView import life.qbic.portal.offermanager.components.person.update.UpdatePersonViewModel import life.qbic.portal.offermanager.components.product.MaintainProductsPresenter import life.qbic.portal.offermanager.components.product.MaintainProductsView @@ -521,9 +522,9 @@ class DependencyManager { } try { - this.updatePersonView = new CreatePersonView(this.updateCustomerController, this.viewModel, this.updatePersonViewModel) + this.updatePersonView = new UpdatePersonView(this.updateCustomerController, this.viewModel, this.updatePersonViewModel) } catch (Exception e) { - log.error("Could not create ${CreatePersonView.getSimpleName()} view.", e) + log.error("Could not create ${UpdatePersonView.getSimpleName()} view.", e) throw e } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonView.groovy index 8294c7002..4b8c76052 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/create/CreatePersonView.groovy @@ -8,6 +8,7 @@ import com.vaadin.data.provider.ListDataProvider import com.vaadin.data.validator.EmailValidator import com.vaadin.icons.VaadinIcons import com.vaadin.server.UserError +import com.vaadin.shared.Registration import com.vaadin.shared.data.sort.SortDirection import com.vaadin.shared.ui.ContentMode import com.vaadin.ui.* @@ -28,9 +29,10 @@ import life.qbic.portal.offermanager.components.AppViewModel @Log4j2 class CreatePersonView extends VerticalLayout { - private final AppViewModel sharedViewModel - private final CreatePersonViewModel createPersonViewModel + protected final AppViewModel sharedViewModel + protected final CreatePersonViewModel createPersonViewModel final CreatePersonController controller + protected Registration submitButtonClickListenerRegistration ComboBox titleField TextField firstNameField @@ -229,7 +231,7 @@ class CreatePersonView extends VerticalLayout { }) } - private void refreshAddressAdditions() { + protected void refreshAddressAdditions() { ListDataProvider dataProvider = this.addressAdditionComboBox.dataProvider as ListDataProvider dataProvider.clearFilters() dataProvider.addFilterByValue({it.organisation }, @@ -322,7 +324,7 @@ class CreatePersonView extends VerticalLayout { * It relies on the separate fields for validation. * @return */ - private boolean allValuesValid() { + protected boolean allValuesValid() { return createPersonViewModel.firstNameValid \ && createPersonViewModel.lastNameValid \ && createPersonViewModel.emailValid \ @@ -330,7 +332,7 @@ class CreatePersonView extends VerticalLayout { } private void registerListeners() { - this.submitButton.addClickListener({ event -> + submitButtonClickListenerRegistration = this.submitButton.addClickListener({ event -> try { // we assume that the view model and the view always contain the same information String title = createPersonViewModel.academicTitle @@ -340,10 +342,7 @@ class CreatePersonView extends VerticalLayout { List affiliations = new ArrayList() affiliations.add(createPersonViewModel.affiliation) - if(createPersonViewModel.outdatedPerson){ - controller.updatePerson(createPersonViewModel.outdatedPerson, firstName, lastName, title, email, affiliations) - } - else{ + if(!createPersonViewModel.outdatedPerson){ controller.createNewPerson(firstName, lastName, title, email, affiliations) } @@ -373,7 +372,7 @@ class CreatePersonView extends VerticalLayout { } - private void updateAffiliationDetails(Affiliation affiliation) { + protected void updateAffiliationDetails(Affiliation affiliation) { if (affiliation) { VerticalLayout content = new VerticalLayout() content.addComponent(new Label("${affiliation.category.value}", ContentMode.HTML)) @@ -394,7 +393,7 @@ class CreatePersonView extends VerticalLayout { /** * Clears User Input from all fields in the Create Person View and reset validation status of all Fields */ - private void clearAllFields() { + protected void clearAllFields() { titleField.clear() firstNameField.clear() diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/search/SearchPersonView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/search/SearchPersonView.groovy index 46feab014..f2326ebb7 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/search/SearchPersonView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/search/SearchPersonView.groovy @@ -94,6 +94,7 @@ class SearchPersonView extends FormLayout{ updatePerson.addClickListener({ viewModel.personEvent.emit(viewModel.selectedPerson) + detailsLayout.setVisible(false) searchPersonLayout.setVisible(false) updatePersonView.setVisible(true) }) diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/update/UpdatePersonView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/update/UpdatePersonView.groovy new file mode 100644 index 000000000..8781a7de2 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/update/UpdatePersonView.groovy @@ -0,0 +1,64 @@ +package life.qbic.portal.offermanager.components.person.update + + +import groovy.util.logging.Log4j2 +import life.qbic.datamodel.dtos.business.Affiliation +import life.qbic.portal.offermanager.components.AppViewModel +import life.qbic.portal.offermanager.components.person.create.CreatePersonController +import life.qbic.portal.offermanager.components.person.create.CreatePersonView + +/** + *

This view is an extension of the {@link CreatePersonView} and adjusts the view components to reflect the update person use case

+ *
+ *

Since both views should look the same changes of the {@link CreatePersonView} should also be reflected in the {@link UpdatePersonView}

+ * + * @since 1.0.0 + * +*/ +@Log4j2 +class UpdatePersonView extends CreatePersonView{ + private final UpdatePersonViewModel updatePersonViewModel + private final AppViewModel sharedViewModel + + + UpdatePersonView(CreatePersonController controller, AppViewModel sharedViewModel, UpdatePersonViewModel updatePersonViewModel) { + super(controller, sharedViewModel, updatePersonViewModel) + this.updatePersonViewModel = updatePersonViewModel + this.sharedViewModel = sharedViewModel + adjustViewElements() + registerListener() + } + + private void adjustViewElements() { + submitButton.caption = "Update Person" + abortButton.caption = "Abort Person Update" + } + + private void registerListener(){ + submitButtonClickListenerRegistration.remove() + submitButton.addClickListener({ + try { + // we assume that the view model and the view always contain the same information + String title = updatePersonViewModel.academicTitle + String firstName = updatePersonViewModel.firstName + String lastName = updatePersonViewModel.lastName + String email = updatePersonViewModel.email + List affiliations = new ArrayList() + affiliations.add(updatePersonViewModel.affiliation) + + if(updatePersonViewModel.outdatedPerson){ + affiliations.addAll(updatePersonViewModel.outdatedPerson.affiliations) + controller.updatePerson(updatePersonViewModel.outdatedPerson, firstName, lastName, title, email, affiliations) + } + + } catch (IllegalArgumentException illegalArgumentException) { + log.error("Illegal arguments for person update. ${illegalArgumentException.getMessage()}") + log.debug("Illegal arguments for person update. ${illegalArgumentException.getMessage()}", illegalArgumentException) + sharedViewModel.failureNotifications.add("Could not update the person. Please verify that your input is correct and try again.") + } catch (Exception e) { + log.error("Unexpected error after person update form submission.", e) + sharedViewModel.failureNotifications.add("An unexpected error occurred. We apologize for any inconveniences. Please inform us via email to support@qbic.zendesk.com.") + } + }) + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/update/UpdatePersonViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/update/UpdatePersonViewModel.groovy index 1a1759ce6..7d727f08f 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/update/UpdatePersonViewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/person/update/UpdatePersonViewModel.groovy @@ -44,10 +44,10 @@ class UpdatePersonViewModel extends CreatePersonViewModel{ } private void loadData(Person person) { - super.academicTitle = person.title - super.firstName = person.firstName - super.lastName = person.lastName - super.email = person.emailAddress - this.affiliation = person.affiliations.first() + academicTitle = person.title + firstName = person.firstName + lastName = person.lastName + email = person.emailAddress + affiliation = person.affiliations.first() } } From ec1f679b005c09bf0a42370dfa5ac50a25cd098b Mon Sep 17 00:00:00 2001 From: Sven F Date: Fri, 26 Mar 2021 13:51:51 +0100 Subject: [PATCH 21/32] Reset view elements after project creation (#433) Solves #432 --- .../projectcreation/CreateProjectView.groovy | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectView.groovy index 6d329c091..018aaaf5b 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/projectcreation/CreateProjectView.groovy @@ -286,12 +286,24 @@ class CreateProjectView extends VerticalLayout{ }) this.model.addPropertyChangeListener("projectCreated", { if (model.getStartedFromView().isPresent()) { + this.resetInputs() this.setVisible(false) this.model.getStartedFromView().get().setVisible(true) } }) } + private void resetInputs() { + this.projectSpaceSelection.clear() + this.desiredProjectCode.clear() + this.resultingProjectCode.clear() + this.desiredSpaceName.clear() + this.resultingSpaceName.clear() + this.existingSpaceLayout.setVisible(false) + this.customSpaceLayout.setVisible(false) + this.projectAvailability.removeAllComponents() + } + private void bindData() { availableSpacesBox.setDataProvider(model.availableSpaces) } From 425b3d5f8a00f5878e38aa6c604a578336aba739 Mon Sep 17 00:00:00 2001 From: wow-such-code Date: Fri, 26 Mar 2021 13:52:50 +0100 Subject: [PATCH 22/32] Hotfix/#400 #417 project creation (#430) Create missing experimental design object necessary to view project in projectbrowser. Co-authored-by: Sven F Co-authored-by: Steffengreiner --- .github/workflows/build_package.yml | 4 +- .github/workflows/run_tests.yml | 4 +- README.rst | 49 +++++++------- .../projects/ProjectDbConnector.groovy | 2 + .../projects/ProjectMainConnector.groovy | 66 +++++++++++++++++++ .../src/main/resources/.gitignore | 1 - 6 files changed, 96 insertions(+), 30 deletions(-) delete mode 100644 offer-manager-app/src/main/resources/.gitignore diff --git a/.github/workflows/build_package.yml b/.github/workflows/build_package.yml index 2dcb496c0..8a28bec84 100644 --- a/.github/workflows/build_package.yml +++ b/.github/workflows/build_package.yml @@ -8,10 +8,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 1.11 + - name: Set up JDK 1.8 uses: actions/setup-java@v1 with: - java-version: 1.11 + java-version: 1.8 - name: Run mvn package run: mvn -B package --file pom.xml diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index fd97ca3c1..c9660cca3 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -8,10 +8,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 1.11 + - name: Set up JDK 1.8 uses: actions/setup-java@v1 with: - java-version: 1.11 + java-version: 1.8 - name: Load local Maven repository cache uses: actions/cache@v2 diff --git a/README.rst b/README.rst index 4b3d58c5b..96883e3f5 100644 --- a/README.rst +++ b/README.rst @@ -1,38 +1,18 @@ -qOffer 2.0 +TOMATO - The Offer MAnager Tool ----------------------------------- -.. image:: https://github.com/qbicsoftware/qOffer_2.0/workflows/Build%20Maven%20Package/badge.svg - :target: https://github.com/qbicsoftware/qOffer_2.0/workflows/Build%20Maven%20Package/badge.svg - :alt: Github Workflow Build Maven Package Status - -.. image:: https://github.com/qbicsoftware/qOffer_2.0/workflows/Run%20Maven%20Tests/badge.svg - :target: https://github.com/qbicsoftware/qOffer_2.0/workflows/Run%20Maven%20Tests/badge.svg - :alt: Github Workflow Tests Status - -.. image:: https://github.com/qbicsoftware/qOffer_2.0/workflows/QUBE%20lint/badge.svg - :target: https://github.com/qbicsoftware/qOffer_2.0/workflows/QUBE%20lint/badge.svg - :alt: qube Lint Status - -.. image:: https://readthedocs.org/projects/qOffer-2.0/badge/?version=latest - :target: https://qOffer-2.0.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status - -.. image:: https://flat.badgen.net/dependabot/thepracticaldev/dev.to?icon=dependabot - :target: https://flat.badgen.net/dependabot/thepracticaldev/dev.to?icon=dependabot - :alt: Dependabot Enabled - +|maven-build| |maven-test| |release| |java| |groovy| -qOffer assists in managing and creating offers +Tomato assists in managing and creating offers * Free software: MIT -* Documentation: https://qOffer-2.0.readthedocs.io. Features -------- * Create new offers -* Add new customers to the database -* Add new affiliations to the database +* Create additional offer versions of existing offers +* Manage person entries and affiliations * Search for offers in the database Usage information @@ -117,3 +97,22 @@ Credits This project was created with qube_. .. _qube: https://github.com/qbicsoftware/qube + +.. |maven-build| image:: https://github.com/qbicsoftware/offer-manager-2-portlet/workflows/Build%20Maven%20Package/badge.svg + :target: https://github.com/qbicsoftware/offer-manager-2-portlet/workflows/Build%20Maven%20Package/badge.svg + :alt: Github Workflow Build Maven Package Status + +.. |maven-test| image:: https://github.com/qbicsoftware/offer-manager-2-portlet/workflows/Run%20Maven%20Tests/badge.svg + :target: https://github.com/qbicsoftware/offer-manager-2-portlet/workflows/Run%20Maven%20Tests/badge.svg + :alt: Github Workflow Tests Status + +.. |release| image:: https://img.shields.io/github/v/release/qbicsoftware/offer-manager-2-portlet.svg + :target: https://github.com/qbicsoftware/offer-manager-2-portlet/release + :alt: Release status + +.. |java| image:: https://img.shields.io/badge/language-java-blue.svg + :alt: Written in Java + +.. |groovy| image:: https://img.shields.io/badge/language-groovy-blue.svg + :alt: Written in Groovy + diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectDbConnector.groovy index 07d19a053..5a56a6829 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectDbConnector.groovy @@ -1,5 +1,6 @@ package life.qbic.portal.offermanager.dataresources.projects +import groovy.transform.CompileStatic import groovy.util.logging.Log4j2 import life.qbic.portal.offermanager.dataresources.persons.PersonDbConnector @@ -9,6 +10,7 @@ import life.qbic.datamodel.dtos.projectmanagement.* import life.qbic.business.projects.create.ProjectExistsException import life.qbic.business.exceptions.DatabaseQueryException import life.qbic.portal.offermanager.dataresources.database.ConnectionProvider + import java.sql.Connection import java.sql.PreparedStatement import java.sql.ResultSet diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectMainConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectMainConnector.groovy index 363ca29ff..07fadb8e7 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectMainConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/projects/ProjectMainConnector.groovy @@ -14,15 +14,33 @@ import life.qbic.datamodel.dtos.business.* import life.qbic.datamodel.dtos.projectmanagement.* import life.qbic.business.exceptions.DatabaseQueryException +import life.qbic.datamodel.identifiers.ExperimentCodeFunctions + +import java.sql.Connection +import java.sql.PreparedStatement +import java.sql.ResultSet +import java.sql.Statement + import life.qbic.openbis.openbisclient.OpenBisClient +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import life.qbic.xml.manager.StudyXMLParser +import life.qbic.xml.study.Qexperiment + import ch.ethz.sis.openbis.generic.asapi.v3.dto.operation.SynchronousOperationExecutionOptions import ch.ethz.sis.openbis.generic.asapi.v3.dto.common.operation.IOperation import ch.ethz.sis.openbis.generic.asapi.v3.dto.project.create.CreateProjectsOperation import ch.ethz.sis.openbis.generic.asapi.v3.dto.project.create.ProjectCreation +import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.create.ExperimentCreation +import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.ExperimentIdentifier +import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.create.CreateExperimentsOperation +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.create.SampleCreation +import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.create.CreateSamplesOperation import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.create.CreateSpacesOperation import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.create.SpaceCreation import ch.ethz.sis.openbis.generic.asapi.v3.dto.space.id.SpacePermId +import ch.ethz.sis.openbis.generic.asapi.v3.dto.entitytype.id.EntityTypePermId import life.qbic.portal.offermanager.dataresources.offers.ProjectAssistant @@ -111,6 +129,53 @@ class ProjectMainConnector implements CreateProjectDataSource, CreateProjectSpac handleOperations(operation) } + private void setupEmptyExperimentalDesign(ProjectSpace space, ProjectCode projectCodeObj) + throws JAXBException { + StudyXMLParser xmlParser = new StudyXMLParser() + JAXBElement res = + xmlParser.createNewDesign(new HashSet<>(), new ArrayList<>(), new HashMap<>(), new HashMap<>()) + String emptyStudyXML = xmlParser.toString(res) + + String spaceCode = space.toString() + String projectCode = projectCodeObj.toString() + + String experimentCode = projectCode + "_INFO" + String sampleCode = projectCode + "000" + String experimentIdentifier = ExperimentCodeFunctions.getInfoExperimentID(spaceCode, projectCode) + + Map properties = new HashMap<>() + properties.put("Q_EXPERIMENTAL_SETUP", emptyStudyXML) + + createOpenbisExperiment(spaceCode, projectCode, experimentCode, "Q_PROJECT_DETAILS", properties) + + createOpenbisSample(spaceCode, experimentIdentifier, sampleCode, "Q_ATTACHMENT_SAMPLE", new HashMap<>()) +} + +private void createOpenbisExperiment(String spaceCode, String projectCode, String experimentCode, String experimentType, Map properties) { + ExperimentCreation experiment = new ExperimentCreation() + experiment.setCode(experimentCode) + experiment.setProjectId(new ch.ethz.sis.openbis.generic.asapi.v3.dto.project.id.ProjectIdentifier(spaceCode, projectCode)) + experiment.setTypeId(new EntityTypePermId(experimentType)) + experiment.setProperties(properties) + + IOperation operation = new CreateExperimentsOperation(experiment) + handleOperations(operation) +} + +private void createOpenbisSample(String spaceCode, String experimentIdentifier, String sampleCode, String sampleType, Map properties) { + SampleCreation sampleCreation = new SampleCreation() + sampleCreation.setTypeId(new EntityTypePermId(sampleType)) + sampleCreation.setSpaceId(new SpacePermId(spaceCode)) + + sampleCreation.setExperimentId(new ExperimentIdentifier(experimentIdentifier)) + sampleCreation.setCode(sampleCode) + + sampleCreation.setProperties(properties) + + IOperation operation = new CreateSamplesOperation(sampleCreation) + handleOperations(operation) +} + /** * Returns a copied list of existing projects fetched upon creation of this class */ @@ -153,6 +218,7 @@ class ProjectMainConnector implements CreateProjectDataSource, CreateProjectSpac } try { createOpenbisProject(space, projectCode, description) + setupEmptyExperimentalDesign(space, projectCode) projectAssistant.linkOfferWithProject(projectApplication.linkedOffer, projectIdentifier) } catch (Exception e) { log.error(e.message) diff --git a/offer-manager-app/src/main/resources/.gitignore b/offer-manager-app/src/main/resources/.gitignore deleted file mode 100644 index b684fa746..000000000 --- a/offer-manager-app/src/main/resources/.gitignore +++ /dev/null @@ -1 +0,0 @@ -developer.properties \ No newline at end of file From 5848ed785079d63d50956f8414d64efaa5993846 Mon Sep 17 00:00:00 2001 From: jnnfr Date: Fri, 26 Mar 2021 14:45:04 +0100 Subject: [PATCH 23/32] Fix/unstable products db connector #382 (#438) Make externally used methods more explicit and internal methods private Co-authored-by: Steffengreiner --- .../products/ProductsDbConnector.groovy | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy index f57f6fdca..951f7b0c9 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy @@ -122,7 +122,13 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData } } - def createOfferItems(List items, int offerId) { + /** + * This method associates an offer with product items. + * + * @param items A list of product items of an offer + * @param offerId An offerId which references the offer containing the list of product items + */ + void createOfferItems(List items, int offerId) { items.each {productItem -> String query = "INSERT INTO productitem (productId, quantity, offerid) "+ "VALUE(?,?,?)" @@ -175,7 +181,7 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData * @param productId String of productId stored in the DB e.g. "DS_1" * @return identifier Long of the iterative identifying part of the productId */ - static long parseProductId(String productId) throws NumberFormatException{ + private static long parseProductId(String productId) throws NumberFormatException{ def splitId = productId.split("_") // The first entry [0] contains the product type which is assigned automatically, no need to parse it. String identifier = splitId[1] @@ -189,7 +195,7 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData * @param product A product for which the type needs to be determined * @return the type of the product or null */ - static String getProductType(Product product){ + private static String getProductType(Product product){ if (product instanceof Sequencing) return 'Sequencing' if (product instanceof ProjectManagement) return 'Project Management' if (product instanceof PrimaryAnalysis) return 'Primary Bioinformatics' From e9ec2e31799b62f5a8f7506ab9b5d440feaa39a8 Mon Sep 17 00:00:00 2001 From: jnnfr Date: Mon, 29 Mar 2021 11:15:23 +0200 Subject: [PATCH 24/32] Add missing button captions (#439) Adds a button caption to make the function of buttons more explicit --- .../components/offer/overview/OfferOverviewView.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy index 07be28ba5..66ea1b7e9 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/overview/OfferOverviewView.groovy @@ -60,8 +60,8 @@ class OfferOverviewView extends FormLayout { this.model = model this.offerOverviewController = offerOverviewController this.overviewGrid = new Grid<>() - this.downloadBtn = new Button(VaadinIcons.DOWNLOAD) - this.updateOfferBtn = new Button(VaadinIcons.EDIT) + this.downloadBtn = new Button("Download Offer",VaadinIcons.DOWNLOAD) + this.updateOfferBtn = new Button("Update Offer",VaadinIcons.EDIT) this.createProjectButton = new Button("Create Project", VaadinIcons.PLUS_CIRCLE) this.downloadSpinner = new ProgressBar() this.createProjectView = createProjectView From 41eecf62bb2f845e3219ccf5752659a892325a63 Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Mon, 29 Mar 2021 16:48:53 +0200 Subject: [PATCH 25/32] Proteomic and Metabolomic Tabs in Product Selection View (#425) * Add Tabs for Proteomic and Metabolomic Products to ProductSelection View and establish connection in viewModel Introduces the Proteomic and Metabolomic ProductCategory Tabs in the ProductSelectionView and the viewModel. Additionally it removes the "product" from all header tabs in the view to make space for the new productCategories --- CHANGELOG.rst | 2 + .../offer/create/CreateOfferViewModel.groovy | 10 ++ .../offer/create/SelectItemsView.groovy | 133 ++++++++++++++++-- 3 files changed, 130 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6fe6b50eb..873466bf2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,8 @@ This project adheres to `Semantic Versioning `_. **Added** +* Proteomic and Metabolomic Products can now be selected and included in an Offer (`#425 `_) + * Link offers to project now. The ``life.qbic.business.offers.Offer`` and ``life.qbic.portal .offermanager.dataresources.offers`` have been extended with a new property to associate it with an existing project by its project identifier. diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy index e5e359e5b..dccb4bced 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/CreateOfferViewModel.groovy @@ -32,6 +32,8 @@ class CreateOfferViewModel { List secondaryAnalysisProducts = new ObservableList(new ArrayList()) List managementProducts = new ObservableList(new ArrayList()) List storageProducts = new ObservableList(new ArrayList()) + List proteomicAnalysisProducts = new ObservableList(new ArrayList()) + List metabolomicAnalysisProduct = new ObservableList(new ArrayList()) ObservableList productItems = new ObservableList(new ArrayList()) ObservableList foundCustomers = new ObservableList(new ArrayList()) @@ -107,6 +109,8 @@ class CreateOfferViewModel { this.primaryAnalysisProducts.clear() this.secondaryAnalysisProducts.clear() this.storageProducts.clear() + this.proteomicAnalysisProducts.clear() + this.metabolomicAnalysisProduct.clear() products.each { product -> ProductItemViewModel productItem = new ProductItemViewModel(0, product) @@ -127,6 +131,12 @@ class CreateOfferViewModel { case DataStorage: storageProducts.add(productItem) break + case ProteomicAnalysis: + proteomicAnalysisProducts.add(productItem) + break + case MetabolomicAnalysis: + metabolomicAnalysisProduct.add(productItem) + break default: // this should not happen throw new RuntimeException("Unknown product category '${product.getClass().getSimpleName()}'") diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/SelectItemsView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/SelectItemsView.groovy index 3eb6e5962..b00ccd46a 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/SelectItemsView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/offer/create/SelectItemsView.groovy @@ -13,17 +13,9 @@ import com.vaadin.ui.VerticalLayout import com.vaadin.ui.components.grid.HeaderRow import com.vaadin.ui.renderers.NumberRenderer import com.vaadin.ui.themes.ValoTheme -import life.qbic.datamodel.dtos.business.services.DataStorage -import life.qbic.datamodel.dtos.business.services.PrimaryAnalysis import life.qbic.datamodel.dtos.business.services.Product -import life.qbic.datamodel.dtos.business.services.ProductUnit -import life.qbic.datamodel.dtos.business.services.ProjectManagement -import life.qbic.datamodel.dtos.business.services.SecondaryAnalysis -import life.qbic.datamodel.dtos.business.services.Sequencing import life.qbic.business.offers.Currency import life.qbic.portal.offermanager.components.GridUtils -import life.qbic.portal.offermanager.components.offer.create.CreateOfferViewModel -import life.qbic.portal.offermanager.components.offer.create.ProductItemViewModel import life.qbic.portal.offermanager.components.AppViewModel /** @@ -47,19 +39,24 @@ class SelectItemsView extends VerticalLayout{ private List storageProduct private List primaryAnalyseProduct private List secondaryAnalyseProduct - + private List proteomicAnalysisProduct + private List metabolomicAnalysisProduct Grid sequencingGrid Grid projectManagementGrid Grid storageGrid Grid primaryAnalyseGrid Grid secondaryAnalyseGrid + Grid proteomicsAnalysisGrid + Grid metabolomicsAnalysisGrid Grid overviewGrid Button applySequencing Button applyProjectManagement Button applyPrimaryAnalysis Button applySecondaryAnalysis + Button applyProteomicAnalysis + Button applyMetabolomicAnalysis Button applyDataStorage Button next Button previous @@ -68,6 +65,8 @@ class SelectItemsView extends VerticalLayout{ TextField amountProjectManagement TextField amountPrimaryAnalysis TextField amountSecondaryAnalysis + TextField amountProteomicAnalysis + TextField amountMetabolomicAnalysis TextField amountDataStorage @@ -110,6 +109,20 @@ class SelectItemsView extends VerticalLayout{ } }) + proteomicAnalysisProduct = createOfferViewModel.proteomicAnalysisProducts as ObservableList + proteomicAnalysisProduct.addPropertyChangeListener({ + if (it instanceof ObservableList.ElementEvent) { + proteomicsAnalysisGrid.dataProvider.refreshAll() + } + }) + + metabolomicAnalysisProduct = createOfferViewModel.metabolomicAnalysisProduct as ObservableList + metabolomicAnalysisProduct.addPropertyChangeListener({ + if (it instanceof ObservableList.ElementEvent) { + metabolomicsAnalysisGrid.dataProvider.refreshAll() + } + }) + initLayout() setupDataProvider() addListener() @@ -123,6 +136,8 @@ class SelectItemsView extends VerticalLayout{ this.sequencingGrid = new Grid<>() this.primaryAnalyseGrid = new Grid<>() this.secondaryAnalyseGrid = new Grid<>() + this.proteomicsAnalysisGrid = new Grid<>() + this.metabolomicsAnalysisGrid = new Grid<>() this.projectManagementGrid = new Grid<>() this.storageGrid = new Grid<>() this.overviewGrid = new Grid<>("Overview:") @@ -133,6 +148,10 @@ class SelectItemsView extends VerticalLayout{ amountPrimaryAnalysis.setPlaceholder("e.g. 1") amountSecondaryAnalysis = new TextField("Quantity:") amountSecondaryAnalysis.setPlaceholder("e.g. 1") + amountProteomicAnalysis = new TextField("Quantity:") + amountProteomicAnalysis.setPlaceholder("e.g. 1") + amountMetabolomicAnalysis = new TextField("Quantity:") + amountMetabolomicAnalysis.setPlaceholder("e.g. 1") amountProjectManagement = new TextField("Quantity:") amountProjectManagement.setPlaceholder("e.g. 1.5") amountDataStorage = new TextField("Quantity:") @@ -145,7 +164,6 @@ class SelectItemsView extends VerticalLayout{ this.previous = new Button(VaadinIcons.CHEVRON_CIRCLE_LEFT) previous.addStyleName(ValoTheme.LABEL_LARGE) - this.applySequencing = new Button("Apply", VaadinIcons.PLUS) applySequencing.setEnabled(false) @@ -155,6 +173,12 @@ class SelectItemsView extends VerticalLayout{ this.applySecondaryAnalysis = new Button("Apply", VaadinIcons.PLUS) applySecondaryAnalysis.setEnabled(false) + this.applyProteomicAnalysis = new Button("Apply", VaadinIcons.PLUS) + applyProteomicAnalysis.setEnabled(false) + + this.applyMetabolomicAnalysis= new Button("Apply", VaadinIcons.PLUS) + applyMetabolomicAnalysis.setEnabled(false) + this.applyDataStorage = new Button("Apply", VaadinIcons.PLUS) applyDataStorage.setEnabled(false) @@ -184,6 +208,18 @@ class SelectItemsView extends VerticalLayout{ VerticalLayout secondaryAnalysisLayout = new VerticalLayout(secondaryAnalyseGrid, quantitySecondary) secondaryAnalysisLayout.setSizeFull() + HorizontalLayout quantityProteomic = new HorizontalLayout(amountProteomicAnalysis,applyProteomicAnalysis) + quantityProteomic.setSizeFull() + quantityProteomic.setComponentAlignment(applyProteomicAnalysis, Alignment.BOTTOM_RIGHT) + VerticalLayout proteomicsLayout = new VerticalLayout(proteomicsAnalysisGrid, quantityProteomic) + proteomicsLayout.setSizeFull() + + HorizontalLayout quantityMetabolomic = new HorizontalLayout(amountMetabolomicAnalysis ,applyMetabolomicAnalysis) + quantityMetabolomic.setSizeFull() + quantityMetabolomic.setComponentAlignment(applyMetabolomicAnalysis, Alignment.BOTTOM_RIGHT) + VerticalLayout metabolomicsLayout = new VerticalLayout(metabolomicsAnalysisGrid, quantityMetabolomic) + metabolomicsLayout.setSizeFull() + HorizontalLayout quantityStorage = new HorizontalLayout(amountDataStorage,applyDataStorage) quantityStorage.setSizeFull() quantityStorage.setComponentAlignment(applyDataStorage, Alignment.BOTTOM_RIGHT) @@ -203,6 +239,8 @@ class SelectItemsView extends VerticalLayout{ generateProductGrid(sequencingGrid) generateProductGrid(primaryAnalyseGrid) generateProductGrid(secondaryAnalyseGrid) + generateProductGrid(proteomicsAnalysisGrid) + generateProductGrid(metabolomicsAnalysisGrid) generateProductGrid(storageGrid) generateProductGrid(projectManagementGrid) // This grid summarises product items selected for this specific offer, so we set quantity = true @@ -213,11 +251,13 @@ class SelectItemsView extends VerticalLayout{ TabSheet packageAccordion = new TabSheet() - packageAccordion.addTab(seqLayout,"Sequencing Products") - packageAccordion.addTab(primaryAnalysisLayout,"Primary Bioinformatics Products") - packageAccordion.addTab(secondaryAnalysisLayout,"Secondary Bioinformatics Products") - packageAccordion.addTab(projectManagementLayout,"Project Management Products") - packageAccordion.addTab(dataStorageLayout,"Data Storage Products") + packageAccordion.addTab(seqLayout,"Sequencing") + packageAccordion.addTab(primaryAnalysisLayout,"Primary Bioinformatics") + packageAccordion.addTab(secondaryAnalysisLayout,"Secondary Bioinformatics") + packageAccordion.addTab(proteomicsLayout,"Proteomics") + packageAccordion.addTab(metabolomicsLayout,"Metabolomics") + packageAccordion.addTab(projectManagementLayout,"Project Management") + packageAccordion.addTab(dataStorageLayout,"Data Storage") this.addComponents(packageAccordion, overview, buttonLayout) this.setSizeFull() @@ -246,6 +286,14 @@ class SelectItemsView extends VerticalLayout{ this.secondaryAnalyseGrid.setDataProvider(secondaryAnalysisProductDataProvider) setupFilters(secondaryAnalysisProductDataProvider, secondaryAnalyseGrid) + ListDataProvider proteomicAnalysisProductDataProvider = new ListDataProvider(createOfferViewModel.proteomicAnalysisProducts) + this.proteomicsAnalysisGrid.setDataProvider(proteomicAnalysisProductDataProvider) + setupFilters(proteomicAnalysisProductDataProvider, proteomicsAnalysisGrid) + + ListDataProvider metabolomicAnalysisProductDataProvider = new ListDataProvider(createOfferViewModel.metabolomicAnalysisProduct) + this.metabolomicsAnalysisGrid.setDataProvider(metabolomicAnalysisProductDataProvider) + setupFilters(metabolomicAnalysisProductDataProvider, metabolomicsAnalysisGrid) + ListDataProvider storageProductDataProvider = new ListDataProvider(createOfferViewModel.storageProducts) this.storageGrid.setDataProvider(storageProductDataProvider) setupFilters(storageProductDataProvider, storageGrid) @@ -387,6 +435,61 @@ class SelectItemsView extends VerticalLayout{ applySecondaryAnalysis.setEnabled(false) }) + proteomicsAnalysisGrid.addSelectionListener({ + applyProteomicAnalysis.setEnabled(true) + }) + + applyProteomicAnalysis.addClickListener({ + if(proteomicsAnalysisGrid.getSelectedItems() != null) { + String amount = amountProteomicAnalysis.getValue() + try{ + if(amount != null && amount.isNumber()) { + proteomicsAnalysisGrid.getSelectedItems().each { + if(Integer.parseInt(amount) >= 0){ + it.setQuantity(Integer.parseInt(amount)) + updateOverviewGrid(it) + } + } + proteomicsAnalysisGrid.getDataProvider().refreshAll() + } + } catch(NumberFormatException e) { + viewModel.failureNotifications.add("The quantity must be an integer number bigger than 0") + } catch (Exception e) { + viewModel.failureNotifications.add("Ups, something went wrong. Please contact support@qbic.zendesk.com") + } + } + amountProteomicAnalysis.clear() + proteomicsAnalysisGrid.deselectAll() + applyProteomicAnalysis.setEnabled(false) + }) + + metabolomicsAnalysisGrid.addSelectionListener({ + applyMetabolomicAnalysis.setEnabled(true) + }) + applyMetabolomicAnalysis.addClickListener({ + if(metabolomicsAnalysisGrid.getSelectedItems() != null) { + String amount = amountMetabolomicAnalysis.getValue() + try{ + if(amount != null && amount.isNumber()) { + metabolomicsAnalysisGrid.getSelectedItems().each { + if(Integer.parseInt(amount) >= 0){ + it.setQuantity(Integer.parseInt(amount)) + updateOverviewGrid(it) + } + } + metabolomicsAnalysisGrid.getDataProvider().refreshAll() + } + } catch(NumberFormatException e) { + viewModel.failureNotifications.add("The quantity must be an integer number bigger than 0") + } catch (Exception e) { + viewModel.failureNotifications.add("Ups, something went wrong. Please contact support@qbic.zendesk.com") + } + } + amountMetabolomicAnalysis.clear() + metabolomicsAnalysisGrid.deselectAll() + applyMetabolomicAnalysis.setEnabled(false) + }) + projectManagementGrid.addSelectionListener({ applyProjectManagement.setEnabled(true) }) From ad62ee6f219c622c39968d929c292c1a3992dd6c Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Mon, 29 Mar 2021 16:50:03 +0200 Subject: [PATCH 26/32] Introduce Metabolomic and Proteomic ProductCategories in Offer PDF (#420) Also adapts the Offer PDF Layout to account for the new productCategories into the individual product groups --- CHANGELOG.rst | 1 + .../offermanager/OfferToPDFConverter.groovy | 32 +++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 873466bf2..87858679f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,7 @@ use cases of the product maintenance and creation feature(`#411 `_) * Add the UpdatePersonView to separate the Update and Create Person use cases more consequently (`#436 `_) +* Proteomic and Metabolomic Products are now included in the Offer PDF **Fixed** diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/OfferToPDFConverter.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/OfferToPDFConverter.groovy index d8f1d506a..4595e6241 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/OfferToPDFConverter.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/OfferToPDFConverter.groovy @@ -10,8 +10,10 @@ import life.qbic.datamodel.dtos.business.ProjectManager import life.qbic.business.offers.Currency import life.qbic.business.offers.OfferExporter import life.qbic.datamodel.dtos.business.services.DataStorage +import life.qbic.datamodel.dtos.business.services.MetabolomicAnalysis import life.qbic.datamodel.dtos.business.services.PrimaryAnalysis import life.qbic.datamodel.dtos.business.services.ProjectManagement +import life.qbic.datamodel.dtos.business.services.ProteomicAnalysis import life.qbic.datamodel.dtos.business.services.SecondaryAnalysis import life.qbic.datamodel.dtos.business.services.Sequencing import org.jsoup.nodes.Document @@ -105,6 +107,13 @@ class OfferToPDFConverter implements OfferExporter { return this.name; } } + /** + * Product group mapping + * + * This map represents the grouping of the different product categories in the offer pdf + * + */ + private final Map productGroupClasses = [:] OfferToPDFConverter(Offer offer) { this.offer = Objects.requireNonNull(offer, "Offer object must not be a null reference") @@ -141,6 +150,7 @@ class OfferToPDFConverter implements OfferExporter { setProjectInformation() setCustomerInformation() setManagerInformation() + setProductGroupMapping() setSelectedItems() setTotalPrices() setQuotationDetails() @@ -187,6 +197,13 @@ class OfferToPDFConverter implements OfferExporter { htmlContent.getElementById("project-manager-email").text(pm.emailAddress) } + void setProductGroupMapping() { + + productGroupClasses[ProductGroups.DATA_GENERATION] = [Sequencing] + productGroupClasses[ProductGroups.DATA_ANALYSIS] = [PrimaryAnalysis, SecondaryAnalysis, ProteomicAnalysis, MetabolomicAnalysis] + productGroupClasses[ProductGroups.DATA_MANAGEMENT] = [ProjectManagement, DataStorage] + } + void setSelectedItems() { // Let's clear the existing item template content first htmlContent.getElementById("product-items-1").empty() @@ -269,11 +286,14 @@ class OfferToPDFConverter implements OfferExporter { } double calculateOverheadSum(List productItems) { - double overheadSum = 0 + double overheadSum productItems.each { - if (it.product instanceof PrimaryAnalysis || it.product instanceof SecondaryAnalysis || it.product instanceof Sequencing) { - overheadSum += it.quantity * it.product.unitPrice * offer.overheadRatio + if (it.product.class in productGroupClasses[ProductGroups.DATA_MANAGEMENT]) { + overheadSum = 0 } + else { + overheadSum += it.quantity * it.product.unitPrice * offer.overheadRatio + } } return overheadSum } @@ -289,13 +309,13 @@ class OfferToPDFConverter implements OfferExporter { // Sort ProductItems into "DataGeneration", "Data Analysis" and "Project & Data Management" productItems.each { - if (it.product instanceof Sequencing) { + if (it.product.class in productGroupClasses[ProductGroups.DATA_GENERATION]) { dataGenerationItems.add(it) } - if (it.product instanceof PrimaryAnalysis || it.product instanceof SecondaryAnalysis) { + if (it.product.class in productGroupClasses[ProductGroups.DATA_ANALYSIS]) { dataAnalysisItems.add(it) } - if (it.product instanceof DataStorage || it.product instanceof ProjectManagement) { + if (it.product.class in productGroupClasses[ProductGroups.DATA_MANAGEMENT]) { dataManagementItems.add(it) } } From 84e6074fa484b23070f7298db385d972a93a2aea Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Tue, 30 Mar 2021 09:04:46 +0200 Subject: [PATCH 27/32] Set up CopyProductView and CopyProductViewModel (#441) Introduces the CopyProductView and Viewmodel as an extension to the CreateProductView and CreateProductViewModel. Additionally establishes the backend connection to enable the View to function as intended Co-authored-by: jnnfr --- .../offermanager/DependencyManager.groovy | 38 ++++++++----- .../product/MaintainProductsController.groovy | 53 +++++++++++++++---- .../product/MaintainProductsPresenter.groovy | 10 +++- .../product/MaintainProductsView.groovy | 42 +++++++++++++-- .../product/MaintainProductsViewModel.groovy | 9 +++- .../product/copy/CopyProductView.groovy | 46 ++++++++++++++++ .../product/copy/CopyProductViewModel.groovy | 51 ++++++++++++++++++ .../product/create/CreateProductView.groovy | 25 +++++---- .../products/ProductsDbConnector.groovy | 3 +- 9 files changed, 233 insertions(+), 44 deletions(-) create mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/copy/CopyProductView.groovy create mode 100644 offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/copy/CopyProductViewModel.groovy diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy index 885ca6505..65124ace3 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/DependencyManager.groovy @@ -3,6 +3,7 @@ package life.qbic.portal.offermanager import groovy.util.logging.Log4j2 import life.qbic.business.offers.fetch.FetchOffer import life.qbic.business.products.archive.ArchiveProduct +import life.qbic.business.products.copy.CopyProduct import life.qbic.business.products.create.CreateProduct import life.qbic.business.projects.create.CreateProject import life.qbic.datamodel.dtos.business.AcademicTitle @@ -11,6 +12,7 @@ import life.qbic.business.persons.affiliation.create.CreateAffiliation import life.qbic.business.persons.create.CreatePerson import life.qbic.business.offers.create.CreateOffer import life.qbic.datamodel.dtos.business.Offer +import life.qbic.datamodel.dtos.business.services.Product import life.qbic.datamodel.dtos.general.Person import life.qbic.datamodel.dtos.projectmanagement.Project import life.qbic.portal.offermanager.communication.EventEmitter @@ -27,6 +29,8 @@ import life.qbic.portal.offermanager.components.person.update.UpdatePersonViewMo import life.qbic.portal.offermanager.components.product.MaintainProductsPresenter import life.qbic.portal.offermanager.components.product.MaintainProductsView import life.qbic.portal.offermanager.components.product.MaintainProductsViewModel +import life.qbic.portal.offermanager.components.product.copy.CopyProductView +import life.qbic.portal.offermanager.components.product.copy.CopyProductViewModel import life.qbic.portal.offermanager.components.product.create.CreateProductView import life.qbic.portal.offermanager.components.product.create.CreateProductViewModel import life.qbic.portal.offermanager.components.product.MaintainProductsController @@ -101,7 +105,7 @@ class DependencyManager { private MaintainProductsViewModel maintainProductsViewModel private MaintainProductsViewModel maintainProductsViewModelArchive private CreateProductViewModel createProductViewModel - private CreateProductViewModel copyProductViewModel + private CopyProductViewModel copyProductViewModel private CreateProjectViewModel createProjectModel private AppPresenter presenter @@ -114,6 +118,7 @@ class DependencyManager { private OfferOverviewPresenter offerOverviewPresenter private MaintainProductsPresenter createProductPresenter private MaintainProductsPresenter archiveProductPresenter + private MaintainProductsPresenter copyProductPresenter private CreateProjectPresenter createProjectPresenter private PersonDbConnector customerDbConnector @@ -135,6 +140,7 @@ class DependencyManager { private FetchOffer fetchOfferUpdateOffer private CreateProduct createProduct private ArchiveProduct archiveProduct + private CopyProduct copyProduct private CreatePersonController createCustomerController private CreatePersonController updateCustomerController @@ -165,7 +171,7 @@ class DependencyManager { private ProjectResourceService projectResourceService private EventEmitter personUpdateEvent private EventEmitter projectCreatedEvent - + private EventEmitter productUpdateEvent /** * Public constructor. * @@ -242,6 +248,7 @@ class DependencyManager { private void setupEventEmitter(){ this.offerUpdateEvent = new EventEmitter() this.personUpdateEvent = new EventEmitter() + this.productUpdateEvent = new EventEmitter() } private void setupViewModels() { @@ -342,13 +349,13 @@ class DependencyManager { } try { - this.maintainProductsViewModel = new MaintainProductsViewModel(productsResourcesService) + this.maintainProductsViewModel = new MaintainProductsViewModel(productsResourcesService, productUpdateEvent) }catch (Exception e) { log.error("Unexpected exception during ${MaintainProductsViewModel.getSimpleName()} view model setup.", e) } try { - this.maintainProductsViewModelArchive = new MaintainProductsViewModel(productsResourcesService) + this.maintainProductsViewModelArchive = new MaintainProductsViewModel(productsResourcesService, productUpdateEvent) }catch (Exception e) { log.error("Unexpected exception during ${MaintainProductsViewModel.getSimpleName()} view model setup.", e) } @@ -360,9 +367,9 @@ class DependencyManager { } try { - this.copyProductViewModel = new CreateProductViewModel() + this.copyProductViewModel = new CopyProductViewModel(productUpdateEvent) }catch (Exception e) { - log.error("Unexpected exception during ${CreateProductViewModel.getSimpleName()} view model setup.", e) + log.error("Unexpected exception during ${CopyProductViewModel.getSimpleName()} view model setup.", e) } } @@ -431,6 +438,11 @@ class DependencyManager { } catch (Exception e) { log.error("Unexpected exception during ${MaintainProductsPresenter.getSimpleName()} setup", e) } + try { + this.copyProductPresenter = new MaintainProductsPresenter(this.maintainProductsViewModel, this.viewModel) + } catch (Exception e) { + log.error("Unexpected exception during ${MaintainProductsPresenter.getSimpleName()} setup", e) + } try { this.createProjectPresenter = new CreateProjectPresenter(createProjectModel, viewModel, projectCreatedEvent) } catch (Exception e) { @@ -454,7 +466,7 @@ class DependencyManager { this.createProduct = new CreateProduct(productsDbConnector,createProductPresenter) this.archiveProduct = new ArchiveProduct(productsDbConnector,archiveProductPresenter) - + this.copyProduct = new CopyProduct(productsDbConnector, copyProductPresenter, productsDbConnector) this.createProject = new CreateProject(createProjectPresenter, projectMainConnector, projectMainConnector) } @@ -500,7 +512,7 @@ class DependencyManager { } try { - this.maintainProductController = new MaintainProductsController(this.createProduct, this.archiveProduct) + this.maintainProductController = new MaintainProductsController(this.createProduct, this.archiveProduct, this.copyProduct) } catch (Exception e) { log.error("Unexpected exception during ${MaintainProductsController.getSimpleName()} setup", e) } @@ -602,17 +614,17 @@ class DependencyManager { throw e } - /**CreateProductView copyProductView + CopyProductView copyProductView try{ - copyProductView = new CreateProductView(copyProductViewModel,maintainProductController) + copyProductView = new CopyProductView(copyProductViewModel, maintainProductController) }catch(Exception e){ - log.error("Could not create ${CreateProductView.getSimpleName()} view.", e) + log.error("Could not create ${CopyProductView.getSimpleName()} view.", e) throw e - }**/ + } MaintainProductsView maintainProductsView try{ - maintainProductsView = new MaintainProductsView(maintainProductsViewModel,createProductView,maintainProductController) + maintainProductsView = new MaintainProductsView(maintainProductsViewModel, createProductView, copyProductView, maintainProductController) }catch (Exception e) { log.error("Could not create ${MaintainProductsView.getSimpleName()} view.", e) throw e diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsController.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsController.groovy index 6fd78de11..1f7d3cb13 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsController.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsController.groovy @@ -4,17 +4,12 @@ import life.qbic.business.logging.Logger import life.qbic.business.logging.Logging import life.qbic.business.products.Converter import life.qbic.business.products.archive.ArchiveProductInput +import life.qbic.business.products.copy.CopyProductInput import life.qbic.business.products.create.CreateProductInput - import life.qbic.datamodel.dtos.business.ProductCategory import life.qbic.datamodel.dtos.business.ProductId -import life.qbic.datamodel.dtos.business.services.DataStorage -import life.qbic.datamodel.dtos.business.services.PrimaryAnalysis import life.qbic.datamodel.dtos.business.services.Product import life.qbic.datamodel.dtos.business.services.ProductUnit -import life.qbic.datamodel.dtos.business.services.ProjectManagement -import life.qbic.datamodel.dtos.business.services.SecondaryAnalysis -import life.qbic.datamodel.dtos.business.services.Sequencing /** *

Controls how the information flows into the use cases {@link life.qbic.business.products.create.CreateProduct} and {@link life.qbic.business.products.archive.ArchiveProduct}

@@ -28,12 +23,15 @@ class MaintainProductsController { private final CreateProductInput createProductInput private final ArchiveProductInput archiveProductInput + private final CopyProductInput copyProductInput private static final Logging log = Logger.getLogger(this.class) MaintainProductsController(CreateProductInput createProductInput, - ArchiveProductInput archiveProductInput){ + ArchiveProductInput archiveProductInput, + CopyProductInput copyProductInput){ this.createProductInput = createProductInput this.archiveProductInput = archiveProductInput + this.copyProductInput = copyProductInput } /** @@ -68,7 +66,27 @@ class MaintainProductsController { } } - private static class ProductConverter{ + /** + * Triggers the copy use case of a product + * + * @param category The products category which determines what kind of product is created + * @param description The description of the product + * @param name The name of the product + * @param unitPrice The unit price of the product + * @param unit The unit in which the product is measured + * @param productId the productId of the to be copied product + */ + void copyProduct(ProductCategory category, String description, String name, double unitPrice, ProductUnit unit, ProductId productId){ + try{ + Product product = ProductConverter.createProductWithVersion(category, description, name, unitPrice, unit, productId.uniqueId) + copyProductInput.copyModified(product) + }catch(Exception unexpected){ + log.error("Unexpected exception at copy product call", unexpected) + throw new IllegalArgumentException("Could not copy product from provided arguments.") + } + } + + private static class ProductConverter { /** * Creates a product DTO based on its products category @@ -80,10 +98,23 @@ class MaintainProductsController { * @param unit The unit in which the product is measured * @return */ - static Product createProduct(ProductCategory category, String description, String name, double unitPrice, ProductUnit unit){ - return Converter.createProduct(category,name, description, unitPrice,unit) + static Product createProduct(ProductCategory category, String description, String name, double unitPrice, ProductUnit unit) { + return Converter.createProduct(category, name, description, unitPrice, unit) } + /** + * Creates a product DTO based on its products category and its ProductID + * + * @param category The products category which determines what kind of products is created + * @param description The description of the product + * @param name The name of the product + * @param unitPrice The unit price of the product + * @param unit The unit in which the product is measured + * @param productId the productID of the previous selected product + * @return + */ + static Product createProductWithVersion(ProductCategory category, String description, String name, double unitPrice, ProductUnit unit, long runningNumber) { + return Converter.createProductWithVersion(category, name, description, unitPrice, unit, runningNumber) + } } - } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsPresenter.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsPresenter.groovy index 8963b33ff..25921870d 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsPresenter.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsPresenter.groovy @@ -1,6 +1,7 @@ package life.qbic.portal.offermanager.components.product import life.qbic.business.products.archive.ArchiveProductOutput +import life.qbic.business.products.copy.CopyProductOutput import life.qbic.business.products.create.CreateProductOutput import life.qbic.datamodel.dtos.business.services.Product import life.qbic.portal.offermanager.components.AppViewModel @@ -13,7 +14,7 @@ import life.qbic.portal.offermanager.components.AppViewModel * @since 1.0.0 * */ -class MaintainProductsPresenter implements CreateProductOutput, ArchiveProductOutput{ +class MaintainProductsPresenter implements CreateProductOutput, ArchiveProductOutput, CopyProductOutput{ private final MaintainProductsViewModel productsViewModel private final AppViewModel mainViewModel @@ -35,6 +36,12 @@ class MaintainProductsPresenter implements CreateProductOutput, ArchiveProductOu productsViewModel.productsResourcesService.addToResource(product) } + @Override + void copied(Product product) { +mainViewModel.successNotifications << "Successfully copied product $product.productId - $product.productName." + productsViewModel.productsResourcesService.addToResource(product) + } + @Override void foundDuplicate(Product product) { mainViewModel.failureNotifications << "Found duplicate product for $product.productId - $product.productName." @@ -45,4 +52,5 @@ class MaintainProductsPresenter implements CreateProductOutput, ArchiveProductOu void failNotification(String notification) { mainViewModel.failureNotifications << notification } + } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsView.groovy index edb96240f..f940f8a8e 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsView.groovy @@ -12,9 +12,9 @@ import com.vaadin.ui.VerticalLayout import com.vaadin.ui.components.grid.HeaderRow import com.vaadin.ui.themes.ValoTheme import life.qbic.business.offers.Currency -import life.qbic.business.products.archive.ArchiveProduct import life.qbic.datamodel.dtos.business.services.Product import life.qbic.portal.offermanager.components.GridUtils +import life.qbic.portal.offermanager.components.product.copy.CopyProductView import life.qbic.portal.offermanager.components.product.create.CreateProductView import life.qbic.portal.offermanager.dataresources.offers.OfferOverview @@ -42,12 +42,15 @@ class MaintainProductsView extends VerticalLayout{ VerticalLayout maintenanceLayout CreateProductView createProductView + CopyProductView copyProductView MaintainProductsView(MaintainProductsViewModel viewModel, CreateProductView createProductView, + CopyProductView copyProductView, MaintainProductsController controller){ this.controller = controller this.viewModel = viewModel this.createProductView = createProductView + this.copyProductView = copyProductView setupPanel() createButtons() @@ -68,8 +71,9 @@ class MaintainProductsView extends VerticalLayout{ private void createButtons(){ addProduct = new Button("Add Product", VaadinIcons.PLUS) copyProduct = new Button ("Copy Product", VaadinIcons.COPY) - copyProduct.setEnabled(false) archiveProduct = new Button("Archive Product", VaadinIcons.ARCHIVE) + copyProduct.setEnabled(false) + archiveProduct.setEnabled(false) buttonLayout = new HorizontalLayout(productDescription, addProduct,copyProduct,archiveProduct) buttonLayout.setMargin(false) @@ -124,8 +128,10 @@ class MaintainProductsView extends VerticalLayout{ } private void addSubViews(){ - this.addComponents(createProductView) //todo add the copy product use case view here + this.addComponents(createProductView) + this.addComponent(copyProductView) createProductView.setVisible(false) + copyProductView.setVisible(false) } private void updateProductDescription(Product product){ @@ -143,7 +149,8 @@ class MaintainProductsView extends VerticalLayout{ productGrid.addSelectionListener({ if(it.firstSelectedItem.isPresent()){ updateProductDescription(it.firstSelectedItem.get()) - viewModel.selectedProduct = it.firstSelectedItem.get() + viewModel.selectedProduct = it.firstSelectedItem + checkProductSelected() } }) @@ -157,14 +164,39 @@ class MaintainProductsView extends VerticalLayout{ createProductView.setVisible(false) }) + copyProduct.addClickListener({ + viewModel.productUpdate.emit(viewModel.selectedProduct.get()) + maintenanceLayout.setVisible(false) + copyProductView.setVisible(true) + }) + + copyProductView.abortButton.addClickListener({ + maintenanceLayout.setVisible(true) + copyProductView.setVisible(false) + }) + + copyProductView.createProductButton.addClickListener({ + maintenanceLayout.setVisible(true) + copyProductView.setVisible(false) + }) + archiveProduct.addClickListener({ - controller.archiveProduct(viewModel.selectedProduct.productId) + controller.archiveProduct(viewModel.selectedProduct.get().productId) }) viewModel.products.addPropertyChangeListener({ productGrid.dataProvider.refreshAll() }) + } + private void checkProductSelected() { + if (viewModel.selectedProduct.get()) { + copyProduct.setEnabled(true) + archiveProduct.setEnabled(true) + } else { + copyProduct.setEnabled(false) + archiveProduct.setEnabled(false) + } } } diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsViewModel.groovy index 5bf02c8b8..f3655d15e 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsViewModel.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsViewModel.groovy @@ -2,6 +2,8 @@ package life.qbic.portal.offermanager.components.product import groovy.beans.Bindable import life.qbic.datamodel.dtos.business.services.Product +import life.qbic.datamodel.dtos.general.Person +import life.qbic.portal.offermanager.communication.EventEmitter import life.qbic.portal.offermanager.dataresources.products.ProductsResourcesService @@ -22,12 +24,14 @@ class MaintainProductsViewModel { ObservableList products = new ObservableList(new ArrayList()) - @Bindable Product selectedProduct + @Bindable Optional selectedProduct final ProductsResourcesService productsResourcesService + EventEmitter productUpdate - MaintainProductsViewModel(ProductsResourcesService productsResourcesService) { + MaintainProductsViewModel(ProductsResourcesService productsResourcesService, EventEmitter productUpdate) { this.productsResourcesService = productsResourcesService + this.productUpdate = productUpdate fetchProducts() subscribe() } @@ -46,4 +50,5 @@ class MaintainProductsViewModel { products.clear() products.addAll(productsResourcesService.iterator().toList()) } + } \ No newline at end of file diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/copy/CopyProductView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/copy/CopyProductView.groovy new file mode 100644 index 000000000..31080efe1 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/copy/CopyProductView.groovy @@ -0,0 +1,46 @@ +package life.qbic.portal.offermanager.components.product.copy + +import com.vaadin.event.MouseEvents +import com.vaadin.ui.Button +import life.qbic.datamodel.dtos.business.ProductId +import life.qbic.datamodel.dtos.business.services.Product +import life.qbic.portal.offermanager.components.product.MaintainProductsController +import life.qbic.portal.offermanager.components.product.create.CreateProductView + + +/** + * This class represents the GUI for copying a product + * + * The view is similar to the {@link CreateProductView} and updates the view to fit the copy product use case + * + * @since: 1.0.0 + * + */ + +class CopyProductView extends CreateProductView { + + CopyProductViewModel copyProductViewModel + MaintainProductsController controller + + CopyProductView(CopyProductViewModel copyProductViewModel, MaintainProductsController controller) { + super(copyProductViewModel, controller) + this.copyProductViewModel = copyProductViewModel + this.controller = controller + adaptView() + adaptListener() + } + + private void adaptView() { + createProductButton.setCaption("Copy Product") + titleLabel.setValue("Copy Service Product") + } + + private void adaptListener() { + createProductButtonRegistration.remove() + this.createProductButton.addClickListener({ + controller.copyProduct(viewModel.productCategory, viewModel.productDescription, viewModel.productName, Double.parseDouble(viewModel.productUnitPrice), viewModel.productUnit, copyProductViewModel.productId) + clearAllFields() + }) + } + +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/copy/CopyProductViewModel.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/copy/CopyProductViewModel.groovy new file mode 100644 index 000000000..8cddf1690 --- /dev/null +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/copy/CopyProductViewModel.groovy @@ -0,0 +1,51 @@ +package life.qbic.portal.offermanager.components.product.copy + +import life.qbic.business.products.Converter +import life.qbic.datamodel.dtos.business.Offer +import life.qbic.datamodel.dtos.business.ProductCategory +import life.qbic.datamodel.dtos.business.ProductId +import life.qbic.datamodel.dtos.business.services.DataStorage +import life.qbic.datamodel.dtos.business.services.MetabolomicAnalysis +import life.qbic.datamodel.dtos.business.services.PrimaryAnalysis +import life.qbic.datamodel.dtos.business.services.Product +import life.qbic.datamodel.dtos.business.services.ProjectManagement +import life.qbic.datamodel.dtos.business.services.ProteomicAnalysis +import life.qbic.datamodel.dtos.business.services.SecondaryAnalysis +import life.qbic.datamodel.dtos.business.services.Sequencing +import life.qbic.datamodel.dtos.general.Person +import life.qbic.portal.offermanager.communication.EventEmitter +import life.qbic.portal.offermanager.components.offer.create.ProductItemViewModel +import life.qbic.portal.offermanager.components.product.MaintainProductsViewModel +import life.qbic.portal.offermanager.components.product.create.CreateProductViewModel +import life.qbic.portal.offermanager.dataresources.persons.CustomerResourceService +import life.qbic.portal.offermanager.dataresources.persons.ProjectManagerResourceService +import life.qbic.portal.offermanager.dataresources.products.ProductsResourcesService + + +/** + *

Holds all values that the user specifies in the CreateProductView

+ * + * @since 1.0.0 + * +*/ +class CopyProductViewModel extends CreateProductViewModel{ + + EventEmitter productUpdate + ProductId productId + CopyProductViewModel(EventEmitter productUpdate) { + super() + this.productUpdate = productUpdate + this.productUpdate.register((Product product) -> { + loadData(product) + }) + } + + private void loadData(Product product) { + productName = product.productName + productDescription = product.description + productUnit = product.unit + productUnitPrice = product.unitPrice + productCategory = Converter.getCategory(product) + productId = product.productId + } +} diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/create/CreateProductView.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/create/CreateProductView.groovy index 76d11d60f..b07dad7ed 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/create/CreateProductView.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/create/CreateProductView.groovy @@ -6,6 +6,7 @@ import com.vaadin.data.ValueContext import com.vaadin.data.validator.RegexpValidator import com.vaadin.icons.VaadinIcons import com.vaadin.server.UserError +import com.vaadin.shared.Registration import com.vaadin.ui.Alignment import com.vaadin.ui.Button import com.vaadin.ui.ComboBox @@ -28,8 +29,8 @@ import life.qbic.portal.offermanager.components.product.MaintainProductsControll */ class CreateProductView extends HorizontalLayout{ - private final CreateProductViewModel viewModel - private final MaintainProductsController controller + protected final CreateProductViewModel viewModel + protected final MaintainProductsController controller TextField productNameField TextField productDescriptionField @@ -37,9 +38,11 @@ class CreateProductView extends HorizontalLayout{ ComboBox productUnitComboBox ComboBox productCategoryComboBox + Button abortButton Button createProductButton - Button abortButton + Registration createProductButtonRegistration + Label titleLabel CreateProductView(CreateProductViewModel createProductViewModel, MaintainProductsController controller){ this.controller = controller @@ -55,16 +58,16 @@ class CreateProductView extends HorizontalLayout{ } private void initLayout(){ - Label label = new Label("Create Service Product") - label.setStyleName(ValoTheme.LABEL_HUGE) - this.addComponent(label) + titleLabel = new Label("Create Service Product") + titleLabel.setStyleName(ValoTheme.LABEL_HUGE) + this.addComponent(titleLabel) //add textfields and boxes HorizontalLayout sharedLayout = new HorizontalLayout(productUnitPriceField,productUnitComboBox) sharedLayout.setWidthFull() HorizontalLayout buttons = new HorizontalLayout(abortButton,createProductButton) - VerticalLayout sideLayout = new VerticalLayout(label,productNameField,productDescriptionField,sharedLayout,productCategoryComboBox,buttons) + VerticalLayout sideLayout = new VerticalLayout(titleLabel,productNameField,productDescriptionField,sharedLayout,productCategoryComboBox,buttons) sideLayout.setSizeFull() sideLayout.setComponentAlignment(buttons, Alignment.BOTTOM_RIGHT) @@ -266,7 +269,7 @@ class CreateProductView extends HorizontalLayout{ * It relies on the separate fields for validation. * @return */ - private boolean allValuesValid() { + protected boolean allValuesValid() { return viewModel.productNameValid \ && viewModel.productDescriptionValid \ && viewModel.productUnitValid \ @@ -275,9 +278,9 @@ class CreateProductView extends HorizontalLayout{ } private void setupListeners(){ - abortButton.addClickListener({ clearAllFields() }) - createProductButton.addClickListener({ + abortButton.addClickListener({clearAllFields() }) + createProductButtonRegistration = this.createProductButton.addClickListener({ controller.createNewProduct(viewModel.productCategory, viewModel.productDescription,viewModel.productName, Double.parseDouble(viewModel.productUnitPrice),viewModel.productUnit) }) @@ -286,7 +289,7 @@ class CreateProductView extends HorizontalLayout{ /** * Clears User Input from all fields in the Create Products View and reset validation status of all Fields */ - private void clearAllFields() { + protected void clearAllFields() { productNameField.clear() productDescriptionField.clear() diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy index 951f7b0c9..e66b84795 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy @@ -4,6 +4,7 @@ import groovy.sql.GroovyRowResult import groovy.util.logging.Log4j2 import life.qbic.business.products.Converter import life.qbic.business.products.archive.ArchiveProductDataSource +import life.qbic.business.products.copy.CopyProductDataSource import life.qbic.business.products.create.CreateProductDataSource import life.qbic.business.products.create.ProductExistsException import life.qbic.datamodel.dtos.business.ProductCategory @@ -26,7 +27,7 @@ import java.sql.SQLException * @since 1.0.0 */ @Log4j2 -class ProductsDbConnector implements ArchiveProductDataSource, CreateProductDataSource { +class ProductsDbConnector implements ArchiveProductDataSource, CreateProductDataSource, CopyProductDataSource { private final ConnectionProvider provider From a199e34bbf0f045e0264993e1c259dc0a0b69e0c Mon Sep 17 00:00:00 2001 From: jenniferboedker Date: Wed, 31 Mar 2021 10:00:57 +0200 Subject: [PATCH 28/32] update changelog --- CHANGELOG.rst | 1 - .../dataresources/products/ProductsDbConnector.groovy | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 87858679f..a3d8cca7e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,7 +11,6 @@ This project adheres to `Semantic Versioning `_. **Added** * Proteomic and Metabolomic Products can now be selected and included in an Offer (`#425 `_) - * Link offers to project now. The ``life.qbic.business.offers.Offer`` and ``life.qbic.portal .offermanager.dataresources.offers`` have been extended with a new property to associate it with an existing project by its project identifier. diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy index e66b84795..b46320907 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy @@ -320,7 +320,7 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData while(result.next()){ String id = result.getString(1) - latestUniqueId = Long.parseLong(id.split('_')[1]) + if(id) latestUniqueId = Long.parseLong(id.split('_')[1]) } } From 1c091ce53f2001bd4d5cd39742d2b0cb50c3bd11 Mon Sep 17 00:00:00 2001 From: Tobias Koch Date: Tue, 30 Mar 2021 16:06:12 +0200 Subject: [PATCH 29/32] Use ProductCategoryFactory and catch IllegalArgumentExceptions (#444) * Use ProductCategoryFactory and catch IllegalArgumentExceptions This commit introduces the ProductCategoryFactore. It also adds checks for parsing failures for enum values using an EnumFactory. * Fix product category switch in Converter * Add tests for fetch() * Exchange String to enum * Complete tests * Update changelog * Fix tests * Fix changelog format --- CHANGELOG.rst | 52 +++++++- .../products/ProductsDbConnector.groovy | 125 ++++++++++-------- .../products/ProductsDbConnectorSpec.groovy | 36 +++++ .../qbic/business/products/Converter.groovy | 22 +-- 4 files changed, 168 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a3d8cca7e..b90ab523e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,26 +6,34 @@ This project adheres to `Semantic Versioning `_. 1.0.0-alpha.5-SNAPSHOT (2021-03-17) --------------------------- +----------------------------------- **Added** * Proteomic and Metabolomic Products can now be selected and included in an Offer (`#425 `_) + * Link offers to project now. The ``life.qbic.business.offers.Offer`` and ``life.qbic.portal .offermanager.dataresources.offers`` have been extended with a new property to associate it with an existing project by its project identifier. + * Finalized the ``life.qbic.business.products.archive.ArchiveProduct`` and ``life/qbic/business/products/create/CreateProduct.groovy`` -use cases of the product maintenance and creation feature(`#411 `_). + use cases of the product maintenance and creation feature(`#411 `_). + * After a project has been created from an offer, the offer overview is updated accordingly -(`#427 `_) + (`#427 `_) + * Add the UpdatePersonView to separate the Update and Create Person use cases more consequently (`#436 `_) + * Proteomic and Metabolomic Products are now included in the Offer PDF **Fixed** * Popup based Notifications are now properly centered in a liferay-environment(`#428 `_) + * Properly refresh the SearchPersonView after Updating a Person (`#436 `_) +* Products that cannot be read from the database are skipped (`#444 `_) + **Dependencies** **Deprecated** @@ -36,12 +44,17 @@ use cases of the product maintenance and creation feature(`#411 `_) + * Add logging with throwable cause (`#371 `_) + * Introduce distinction of products in the offer PDF according to the associated service data generation, data analysis and project management (`#364 `_) + * Introduce overheadRatio property to life.qbic.business.offers.Offer used to show the applied overhead markup in the pricing footer of the Offer PDF(`#362 `_) + * Introduce first draft for OpenBis based project space and project creation (`#396 `_) + * Introduce first draft for product maintenance and creation (`#392 `_) **Fixed** @@ -69,8 +82,11 @@ use cases of the product maintenance and creation feature(`#411 `_) + * Make the offer controls more intuitive (`#341 `_) + * Update offers without changes is not possible anymore (`#222 `_) + * Rename CreateCustomer and UpdateCustomer classes and methods (`#315 https://github.com/qbicsoftware/offer-manager-2-portlet/issues/315`_) **Dependencies** @@ -85,35 +101,65 @@ use cases of the product maintenance and creation feature(`#411 `_ + * Addressed `#234 `_ + * Addressed `#246 `_ + * Addressed `#260 `_ + * Addressed `#269 `_ + * Addressed `#270 `_ + * Addressed `#271 `_ + * Addressed `#275 `_ + * Addressed `#282 `_ + * Addressed `#295 `_ + * Addressed `#309 `_ + * Replace the project description with project objective (`#339 `_) + * Added support to configure the chromium browser executable. An environment variable `CHROMIUM_ALIAS` has been introduced that can be set to define the chromium executable in the deployment system of the application. Addresses `#336 `_ diff --git a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy index b46320907..ae8b4f29f 100644 --- a/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy +++ b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy @@ -2,17 +2,17 @@ package life.qbic.portal.offermanager.dataresources.products import groovy.sql.GroovyRowResult import groovy.util.logging.Log4j2 +import life.qbic.business.exceptions.DatabaseQueryException import life.qbic.business.products.Converter import life.qbic.business.products.archive.ArchiveProductDataSource import life.qbic.business.products.copy.CopyProductDataSource import life.qbic.business.products.create.CreateProductDataSource import life.qbic.business.products.create.ProductExistsException import life.qbic.datamodel.dtos.business.ProductCategory +import life.qbic.datamodel.dtos.business.ProductCategoryFactory import life.qbic.datamodel.dtos.business.ProductId import life.qbic.datamodel.dtos.business.ProductItem import life.qbic.datamodel.dtos.business.services.* -import life.qbic.business.exceptions.DatabaseQueryException - import life.qbic.portal.offermanager.dataresources.database.ConnectionProvider import org.apache.groovy.sql.extensions.SqlExtensions @@ -31,6 +31,9 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData private final ConnectionProvider provider + private static final ProductCategoryFactory productCategoryFactory = new ProductCategoryFactory() + private static final ProductUnitFactory productUnitFactory = new ProductUnitFactory() + /** * Creates a connector for a MariaDB instance. * @@ -58,14 +61,14 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData try { return fetchAllProductsFromDb() } catch (SQLException e) { - log.error(e.message) - log.error(e.stackTrace.join("\n")) + log.error("Unexpected exception: $e.message") + log.debug("Unexpected exception: $e.message", e) throw new DatabaseQueryException("Unable to list all available products.") } } private List fetchAllProductsFromDb() { - List products = [] + List products = new ArrayList<>() String query = Queries.SELECT_ALL_PRODUCTS + "WHERE active = 1" provider.connect().withCloseable { final PreparedStatement statement = it.prepareStatement(query) @@ -76,51 +79,48 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData } private static List convertResultSet(ResultSet resultSet) { - final def products = [] + final List products = new ArrayList<>() while (resultSet.next()) { - products.add(rowResultToProduct(SqlExtensions.toRowResult(resultSet))) + try { + Product product = rowResultToProduct(SqlExtensions.toRowResult(resultSet)) + products.add(product) + } catch (IllegalArgumentException illegalRow) { + log.warn("Could not parse row. Skipping") + log.debug("Could not parse row. Skipping.", illegalRow) + } } return products } - private static Product rowResultToProduct(GroovyRowResult row) { - def dbProductCategory = row.category - String productId = row.productId - ProductCategory productCategory - switch(dbProductCategory) { - case "Data Storage": - productCategory = ProductCategory.DATA_STORAGE - break - case "Primary Bioinformatics": - productCategory = ProductCategory.PRIMARY_BIOINFO - break - case "Project Management": - productCategory = ProductCategory.PROJECT_MANAGEMENT - break - case "Secondary Bioinformatics": - productCategory = ProductCategory.SECONDARY_BIOINFO - break - case "Sequencing": - productCategory = ProductCategory.SEQUENCING - break - case "Proteomics": - productCategory = ProductCategory.PROTEOMIC - break - case "Metabolomics": - productCategory = ProductCategory.METABOLOMIC - break - } - - if(!productCategory) { - log.error("Product could not be parsed from database query.") - log.error(row) - throw new DatabaseQueryException("Cannot parse product") - } else { - return Converter.createProductWithVersion(productCategory,row.productName as String, - row.description as String, - row.unitPrice as Double, - new ProductUnitFactory().getForString(row.unit as String), parseProductId(productId)) + /** + * + * @param row a GroovyRowResult map + * @return a Product parsed from the provided map + * @throws IllegalArgumentException in case not all fields necessary are found + * or fields could not be parsed + */ + private static Product rowResultToProduct(GroovyRowResult row) throws IllegalArgumentException { + Product product + try { + String description = row.description + ProductCategory productCategory = productCategoryFactory.getForString(row.category as String) + long productId = parseProductId(row.productId as String) + String productName = row.productName + ProductUnit productUnit = productUnitFactory.getForString(row.unit as String) + double unitPrice = row.unitPrice + + product = Converter.createProductWithVersion( + productCategory, + productName, + description, + unitPrice, + productUnit, + productId) + + } catch (NullPointerException | IllegalArgumentException illegalArgument) { + throw new IllegalArgumentException("Could not parse product from provided information.", illegalArgument) } + return product } /** @@ -183,6 +183,9 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData * @return identifier Long of the iterative identifying part of the productId */ private static long parseProductId(String productId) throws NumberFormatException{ + if (!productId.contains("_")) { + throw new IllegalArgumentException("Not a valid product identifier.") + } def splitId = productId.split("_") // The first entry [0] contains the product type which is assigned automatically, no need to parse it. String identifier = splitId[1] @@ -197,13 +200,13 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData * @return the type of the product or null */ private static String getProductType(Product product){ - if (product instanceof Sequencing) return 'Sequencing' - if (product instanceof ProjectManagement) return 'Project Management' - if (product instanceof PrimaryAnalysis) return 'Primary Bioinformatics' - if (product instanceof SecondaryAnalysis) return 'Secondary Bioinformatics' - if (product instanceof DataStorage) return 'Data Storage' - if (product instanceof ProteomicAnalysis) return 'Proteomics' - if (product instanceof MetabolomicAnalysis) return 'Metabolomics' + if (product instanceof Sequencing) return ProductCategory.SEQUENCING.getValue() + if (product instanceof ProjectManagement) return ProductCategory.PROJECT_MANAGEMENT.getValue() + if (product instanceof PrimaryAnalysis) return ProductCategory.PRIMARY_BIOINFO.getValue() + if (product instanceof SecondaryAnalysis) return ProductCategory.SECONDARY_BIOINFO.getValue() + if (product instanceof DataStorage) return ProductCategory.DATA_STORAGE.getValue() + if (product instanceof ProteomicAnalysis) return ProductCategory.PROTEOMIC.getValue() + if (product instanceof MetabolomicAnalysis) return ProductCategory.METABOLOMIC.getValue() return null } @@ -221,10 +224,15 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData statement.setInt(1, offerPrimaryId) ResultSet result = statement.executeQuery() while (result.next()) { - Product product = rowResultToProduct(SqlExtensions.toRowResult(result)) - double quantity = result.getDouble("quantity") - ProductItem item = new ProductItem(quantity, product) - productItems << item + try { + Product product = rowResultToProduct(SqlExtensions.toRowResult(result)) + double quantity = result.getDouble("quantity") + ProductItem item = new ProductItem(quantity, product) + productItems << item + } catch (IllegalArgumentException illegalArgumentException) { + log.warn("Could not parse product. Skipping.") + log.debug("Could not parse product. Skipping.", illegalArgumentException) + } } } return productItems @@ -266,7 +274,12 @@ class ProductsDbConnector implements ArchiveProductDataSource, CreateProductData ResultSet result = preparedStatement.executeQuery() while (result.next()) { - product = Optional.of(rowResultToProduct(SqlExtensions.toRowResult(result))) + try { + product = Optional.of(rowResultToProduct(SqlExtensions.toRowResult(result))) + } catch(IllegalArgumentException illegalArgumentException) { + log.warn("Could not parse product. Skipping.") + log.debug("Could not parse product. Skipping.", illegalArgumentException) + } } } return product diff --git a/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/products/ProductsDbConnectorSpec.groovy b/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/products/ProductsDbConnectorSpec.groovy index f34c8ba89..45e1f3b94 100644 --- a/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/products/ProductsDbConnectorSpec.groovy +++ b/offer-manager-app/src/test/groovy/life/qbic/portal/qoffer2/products/ProductsDbConnectorSpec.groovy @@ -1,6 +1,7 @@ package life.qbic.portal.qoffer2.products import groovy.sql.GroovyRowResult +import life.qbic.datamodel.dtos.business.ProductId import life.qbic.datamodel.dtos.business.services.AtomicProduct import life.qbic.datamodel.dtos.business.services.PrimaryAnalysis import life.qbic.datamodel.dtos.business.services.Product @@ -120,4 +121,39 @@ class ProductsDbConnectorSpec extends Specification { 0 | "Primary Bioinformatics" | "Sample QC with report" | "Sample QC" | 49.99 | ProductUnit.PER_SAMPLE | "1" } + + def "Fetch(life.qbic.datamodel.dtos.business.ProductId) ignores rows with incomplete or uninterpretable information"() { + given: "some expected query results" + GroovyMock(SqlExtensions, global: true) + SqlExtensions.toRowResult(_ as ResultSet) >> new GroovyRowResult( + ["id":id, "category":category, "description":description, "productName": productName, + "unitPrice": unitPrice, "unit": unit, "productId": productId]) + + and: "a result set containing only 6 rows" + ResultSet resultSet = Stub(ResultSet, { + it.next() >>> [true, false] + }) + PreparedStatement statement = Stub(PreparedStatement, { + it.executeQuery() >> resultSet + }) + + and: "a valid connection" + Connection connection = Stub(Connection, {it.prepareStatement(_ as String) >> statement}) + ConnectionProvider provider = Stub(ConnectionProvider, {it.connect() >> connection}) + def connector = new ProductsDbConnector(provider) + + when: "the query is executed" + Optional result = connector.fetch(new ProductId("DS", "1")) + + then: + ! result.isPresent() + + where: "available products information is as follows" + id | category | description | productName | unitPrice | unit | productId + 0 | "Unknown category" | "Sample QC with report" | "Sample QC" | 49.99 | "Sample" | "DS_1" + 1 | "Primary Bioinformatics" | null | "Sample QC with report" | 49.99 | "Sample" | "DS_1" + 2 | "Primary Bioinformatics" | "Sample QC with report" | null | 49.99 | "Sample" | "DS_1" + 4 | "Primary Bioinformatics" | "Sample QC with report" | "Sample QC" | 49.99 | "Unknown Unit" | "DS_1" + 5 | "Primary Bioinformatics" | "Sample QC with report" | "Sample QC" | 49.99 | "Sample" | "This is some random string. Lorem ipsum" + } } diff --git a/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy b/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy index b001ae2af..359618b91 100644 --- a/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy @@ -1,5 +1,7 @@ package life.qbic.business.products +import life.qbic.business.logging.Logger +import life.qbic.business.logging.Logging import life.qbic.datamodel.dtos.business.ProductCategory import life.qbic.datamodel.dtos.business.services.DataStorage import life.qbic.datamodel.dtos.business.services.MetabolomicAnalysis @@ -22,6 +24,8 @@ import life.qbic.datamodel.dtos.business.services.Sequencing */ class Converter { + private static final Logging log = Logger.getLogger(this.class) + /** * Creates a product DTO based on its products category without a version * @@ -49,29 +53,31 @@ class Converter { * @return */ static Product createProductWithVersion(ProductCategory category, String name, String description, double unitPrice, ProductUnit unit, long runningNumber){ - Product product + Product product = null switch (category) { - case "DATA_STORAGE": + case ProductCategory.DATA_STORAGE: product = new DataStorage(name, description, unitPrice,unit, runningNumber.toString()) break - case "PRIMARY_BIOINFO": + case ProductCategory.PRIMARY_BIOINFO: product = new PrimaryAnalysis(name, description, unitPrice,unit, runningNumber.toString()) break - case "PROJECT_MANAGEMENT": + case ProductCategory.PROJECT_MANAGEMENT: product = new ProjectManagement(name, description, unitPrice,unit, runningNumber.toString()) break - case "SECONDARY_BIOINFO": + case ProductCategory.SECONDARY_BIOINFO: product = new SecondaryAnalysis(name, description, unitPrice,unit, runningNumber.toString()) break - case "SEQUENCING": + case ProductCategory.SEQUENCING: product = new Sequencing(name, description, unitPrice,unit, runningNumber.toString()) break - case "PROTEOMIC": + case ProductCategory.PROTEOMIC: product = new ProteomicAnalysis(name, description, unitPrice,unit, runningNumber.toString()) break - case "METABOLOMIC": + case ProductCategory.METABOLOMIC: product = new MetabolomicAnalysis(name, description, unitPrice,unit, runningNumber.toString()) break + default: + log.warn("Unknown product category $category") } if(!product) throw new IllegalArgumentException("Cannot parse product") return product From b3613eb79ace48f3db479aac522245f299c3ffb9 Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Wed, 7 Apr 2021 16:57:10 +0200 Subject: [PATCH 30/32] Update Changelog --- CHANGELOG.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b90ab523e..252ec551c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,17 +14,17 @@ This project adheres to `Semantic Versioning `_. * Link offers to project now. The ``life.qbic.business.offers.Offer`` and ``life.qbic.portal .offermanager.dataresources.offers`` have been extended with a new property to associate it with -an existing project by its project identifier. +an existing project by its project identifier. (`#410 `_). * Finalized the ``life.qbic.business.products.archive.ArchiveProduct`` and ``life/qbic/business/products/create/CreateProduct.groovy`` - use cases of the product maintenance and creation feature(`#411 `_). + use cases of the product maintenance and creation feature (`#411 `_). * After a project has been created from an offer, the offer overview is updated accordingly (`#427 `_) * Add the UpdatePersonView to separate the Update and Create Person use cases more consequently (`#436 `_) -* Proteomic and Metabolomic Products are now included in the Offer PDF +* Proteomic and Metabolomic Products are now included in the Offer PDF (`#420 `_) **Fixed** @@ -61,7 +61,7 @@ an existing project by its project identifier. * User cannot select other offers from the overview anymore, during the offer details are loaded after a selection. Selection is enabled again after the resource has been loaded. This solves a - not yet reported issue that can be observed when dealing with a significant network delay. + not yet reported issue that can be observed when dealing with a significant network delay. (`#374 `_) **Dependencies** From 973f506987e03efd56067f176f9bb93b9f6b7275 Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Wed, 7 Apr 2021 17:30:17 +0200 Subject: [PATCH 31/32] Bump Version to 1.0.0-alpha.5 --- CHANGELOG.rst | 2 +- offer-manager-app/pom.xml | 4 ++-- offer-manager-domain/pom.xml | 2 +- pom.xml | 2 +- qube.cfg | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 252ec551c..07b120652 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,7 +5,7 @@ Changelog This project adheres to `Semantic Versioning `_. -1.0.0-alpha.5-SNAPSHOT (2021-03-17) +1.0.0-alpha.5 (2021-04-07) ----------------------------------- **Added** diff --git a/offer-manager-app/pom.xml b/offer-manager-app/pom.xml index 29d71ce3a..f9eedcc3e 100644 --- a/offer-manager-app/pom.xml +++ b/offer-manager-app/pom.xml @@ -5,7 +5,7 @@ offer-manager life.qbic - 1.0.0-alpha.5-SNAPSHOT + 1.0.0-alpha.5 4.0.0 war @@ -15,7 +15,7 @@ life.qbic offer-manager-domain - 1.0.0-alpha.5-SNAPSHOT + 1.0.0-alpha.5 compile diff --git a/offer-manager-domain/pom.xml b/offer-manager-domain/pom.xml index 71f11e211..056d436bb 100644 --- a/offer-manager-domain/pom.xml +++ b/offer-manager-domain/pom.xml @@ -7,7 +7,7 @@ offer-manager life.qbic - 1.0.0-alpha.5-SNAPSHOT + 1.0.0-alpha.5 diff --git a/pom.xml b/pom.xml index 5ee67667e..51df5d8cd 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ offer-manager-app offer-manager - 1.0.0-alpha.5-SNAPSHOT + 1.0.0-alpha.5 life.qbic The new offer manager http://github.com/qbicsoftware/qOffer_2.0 diff --git a/qube.cfg b/qube.cfg index a831f5b62..fe4a5ec55 100644 --- a/qube.cfg +++ b/qube.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.0-alpha.5-SNAPSHOT +current_version = 1.0.0-alpha.5 [bumpversion_files_whitelisted] dot_qube = .qube.yml From 3d2895f7dc30d14a3db137ee6477ee7934e83379 Mon Sep 17 00:00:00 2001 From: Steffengreiner Date: Wed, 7 Apr 2021 17:36:20 +0200 Subject: [PATCH 32/32] Fix Changelog Typo --- CHANGELOG.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 07b120652..be30411b2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,9 +12,9 @@ This project adheres to `Semantic Versioning `_. * Proteomic and Metabolomic Products can now be selected and included in an Offer (`#425 `_) -* Link offers to project now. The ``life.qbic.business.offers.Offer`` and ``life.qbic.portal -.offermanager.dataresources.offers`` have been extended with a new property to associate it with -an existing project by its project identifier. (`#410 `_). +* Link offers to project now. The ``life.qbic.business.offers.Offer`` and ``life.qbic.portal.offermanager.dataresources.offers`` + have been extended with a new property to associate it with + an existing project by its project identifier. (`#410 `_). * Finalized the ``life.qbic.business.products.archive.ArchiveProduct`` and ``life/qbic/business/products/create/CreateProduct.groovy`` use cases of the product maintenance and creation feature (`#411 `_).