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/CHANGELOG.rst b/CHANGELOG.rst index 6a30e91e6..be30411b2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,25 +4,64 @@ Changelog This project adheres to `Semantic Versioning `_. + +1.0.0-alpha.5 (2021-04-07) +----------------------------------- + +**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. (`#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 `_). + +* 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 (`#420 `_) + +**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** + 1.0.0-alpha.4 (2021-03-16) -------------------------- **Added** * Introduce subtotals in Offer PDF ProductItem Table(`#349 `_) + * 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** * 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** @@ -43,8 +82,11 @@ This project adheres to `Semantic Versioning `_. **Fixed** * Update the agreement section of the offer (`#329 `_) + * 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** @@ -59,35 +101,65 @@ This project adheres to `Semantic Versioning `_. **Added** * Create project with QUBE + * Create project modules infrastructure and domain + * Possibility to list all affiliations stored in the database + * Possibility to list all customers and project managers stored in the database + * Possibility to list all offers stored in the database + * Create and add a new customer to the database + * Create and add a new affiliation to the database + * Create and add a new offer to the database + * Possibility to list all packages stored in the database + * Add the option to create a customer while creating an offer + * Show affiliation details when selecting an affiliation for a customer + * Possibility to filter for customers in table overview + * Show overview over all offers in database + * Possibility to download an offer + * Possibility to abort customer creation + * Dynamic cost overview upon offer creation + * Calculate prices of an offer (VAT, overheads, net price) + * Create an unique offer id + * Addressed `#124 `_ + * 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/pom.xml b/offer-manager-app/pom.xml index b02cd3871..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.4 + 1.0.0-alpha.5 4.0.0 war @@ -15,7 +15,7 @@ life.qbic offer-manager-domain - 1.0.0-alpha.4 + 1.0.0-alpha.5 compile 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..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 @@ -2,6 +2,9 @@ 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 import life.qbic.datamodel.dtos.business.AffiliationCategory @@ -9,7 +12,9 @@ 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 import life.qbic.portal.offermanager.components.offer.overview.OfferOverviewController import life.qbic.portal.offermanager.components.offer.overview.OfferOverviewPresenter @@ -19,11 +24,16 @@ 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 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 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,8 +103,9 @@ class DependencyManager { private SearchPersonViewModel searchPersonViewModel private CreatePersonViewModel createCustomerViewModelNewOffer private MaintainProductsViewModel maintainProductsViewModel + private MaintainProductsViewModel maintainProductsViewModelArchive private CreateProductViewModel createProductViewModel - private CreateProductViewModel copyProductViewModel + private CopyProductViewModel copyProductViewModel private CreateProjectViewModel createProjectModel private AppPresenter presenter @@ -105,6 +116,9 @@ class DependencyManager { private CreateOfferPresenter createOfferPresenter private CreateOfferPresenter updateOfferPresenter private OfferOverviewPresenter offerOverviewPresenter + private MaintainProductsPresenter createProductPresenter + private MaintainProductsPresenter archiveProductPresenter + private MaintainProductsPresenter copyProductPresenter private CreateProjectPresenter createProjectPresenter private PersonDbConnector customerDbConnector @@ -124,6 +138,9 @@ class DependencyManager { private FetchOffer fetchOfferOfferOverview private FetchOffer fetchOfferCreateOffer private FetchOffer fetchOfferUpdateOffer + private CreateProduct createProduct + private ArchiveProduct archiveProduct + private CopyProduct copyProduct private CreatePersonController createCustomerController private CreatePersonController updateCustomerController @@ -132,6 +149,7 @@ class DependencyManager { private CreateOfferController createOfferController private CreateOfferController updateOfferController private OfferOverviewController offerOverviewController + private MaintainProductsController maintainProductController private CreateProjectController createProjectController private CreatePersonView createCustomerView @@ -152,7 +170,8 @@ class DependencyManager { private ProjectSpaceResourceService projectSpaceResourceService private ProjectResourceService projectResourceService private EventEmitter personUpdateEvent - + private EventEmitter projectCreatedEvent + private EventEmitter productUpdateEvent /** * Public constructor. * @@ -202,7 +221,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) @@ -212,7 +234,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) @@ -225,6 +248,7 @@ class DependencyManager { private void setupEventEmitter(){ this.offerUpdateEvent = new EventEmitter() this.personUpdateEvent = new EventEmitter() + this.productUpdateEvent = new EventEmitter() } private void setupViewModels() { @@ -325,7 +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, productUpdateEvent) }catch (Exception e) { log.error("Unexpected exception during ${MaintainProductsViewModel.getSimpleName()} view model setup.", e) } @@ -337,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) } } @@ -397,10 +427,26 @@ class DependencyManager { } catch (Exception e) { log.error("Unexpected exception during ${OfferOverviewPresenter.getSimpleName()} setup", e) } + try { - this.createProjectPresenter = new CreateProjectPresenter(createProjectModel, viewModel) + this.createProductPresenter = new MaintainProductsPresenter(this.maintainProductsViewModel, this.viewModel) } catch (Exception e) { - log.error("Unexpected exception during ${OfferOverviewPresenter.getSimpleName()} setup", 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.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) { + log.error("Unexpected exception during ${CreateProjectPresenter.getSimpleName()} setup", e) } } @@ -418,6 +464,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.copyProduct = new CopyProduct(productsDbConnector, copyProductPresenter, productsDbConnector) this.createProject = new CreateProject(createProjectPresenter, projectMainConnector, projectMainConnector) } @@ -461,10 +510,17 @@ class DependencyManager { } catch (Exception e) { log.error("Unexpected exception during ${OfferOverviewController.getSimpleName()} setup", e) } + try { + this.maintainProductController = new MaintainProductsController(this.createProduct, this.archiveProduct, this.copyProduct) + } 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) } } @@ -478,9 +534,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 } @@ -552,23 +608,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 + CopyProductView copyProductView try{ - copyProductView = new CreateProductView(copyProductViewModel) + 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,copyProductView) + 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/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) } } 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..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()) @@ -90,19 +92,25 @@ 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() this.primaryAnalysisProducts.clear() this.secondaryAnalysisProducts.clear() this.storageProducts.clear() + this.proteomicAnalysisProducts.clear() + this.metabolomicAnalysisProduct.clear() products.each { product -> ProductItemViewModel productItem = new ProductItemViewModel(0, product) @@ -123,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 cf4bc705b..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) @@ -267,26 +315,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 * @@ -407,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) }) 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..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 @@ -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) @@ -180,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() @@ -236,7 +247,7 @@ class OfferOverviewView extends FormLayout { overviewGrid.setEnabled(true) downloadBtn.setEnabled(true) updateOfferBtn.setEnabled(true) - createProjectButton.setEnabled(true) + checkProjectCreationAllowed(offerOverview) ui.setPollInterval(-1) }) } 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/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..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 @@ -284,6 +284,24 @@ class CreateProjectView extends VerticalLayout{ new ProjectCode(model.resultingProjectCode))) } }) + 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() { 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/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() } } 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 56% 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..1f7d3cb13 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,19 +1,15 @@ -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 +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}

@@ -27,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 } /** @@ -63,11 +62,31 @@ 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.") } } - 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 @@ -79,31 +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){ - 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 + 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/products/MaintainProductsPresenter.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/components/product/MaintainProductsPresenter.groovy similarity index 75% 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..25921870d 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,6 +1,7 @@ -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.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 @@ -26,11 +27,19 @@ 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 + void copied(Product product) { +mainViewModel.successNotifications << "Successfully copied product $product.productId - $product.productName." + productsViewModel.productsResourcesService.addToResource(product) } @Override @@ -43,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 8632f73d5..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 @@ -14,6 +14,7 @@ import com.vaadin.ui.themes.ValoTheme import life.qbic.business.offers.Currency 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 @@ -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,11 +42,12 @@ class MaintainProductsView extends VerticalLayout{ VerticalLayout maintenanceLayout CreateProductView createProductView - CreateProductView copyProductView + CopyProductView copyProductView - MaintainProductsView(MaintainProductsViewModel viewModel, CreateProductView createProductView - , CreateProductView copyProductView){ - //todo add the controller + MaintainProductsView(MaintainProductsViewModel viewModel, CreateProductView createProductView, + CopyProductView copyProductView, + MaintainProductsController controller){ + this.controller = controller this.viewModel = viewModel this.createProductView = createProductView this.copyProductView = copyProductView @@ -69,6 +72,8 @@ class MaintainProductsView extends VerticalLayout{ addProduct = new Button("Add Product", VaadinIcons.PLUS) copyProduct = new Button ("Copy Product", VaadinIcons.COPY) archiveProduct = new Button("Archive Product", VaadinIcons.ARCHIVE) + copyProduct.setEnabled(false) + archiveProduct.setEnabled(false) buttonLayout = new HorizontalLayout(productDescription, addProduct,copyProduct,archiveProduct) buttonLayout.setMargin(false) @@ -123,7 +128,8 @@ class MaintainProductsView extends VerticalLayout{ } private void addSubViews(){ - this.addComponents(createProductView,copyProductView) + this.addComponents(createProductView) + this.addComponent(copyProductView) createProductView.setVisible(false) copyProductView.setVisible(false) } @@ -143,6 +149,8 @@ class MaintainProductsView extends VerticalLayout{ productGrid.addSelectionListener({ if(it.firstSelectedItem.isPresent()){ updateProductDescription(it.firstSelectedItem.get()) + viewModel.selectedProduct = it.firstSelectedItem + checkProductSelected() } }) @@ -157,14 +165,38 @@ class MaintainProductsView extends VerticalLayout{ }) copyProduct.addClickListener({ + viewModel.productUpdate.emit(viewModel.selectedProduct.get()) maintenanceLayout.setVisible(false) - copyProduct.setVisible(true) + copyProductView.setVisible(true) + }) + + copyProductView.abortButton.addClickListener({ + maintenanceLayout.setVisible(true) + copyProductView.setVisible(false) + }) + + copyProductView.createProductButton.addClickListener({ + maintenanceLayout.setVisible(true) + copyProductView.setVisible(false) }) archiveProduct.addClickListener({ - //todo use the controller to trigger the use case + 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 a74ea420e..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 @@ -1,6 +1,9 @@ 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 @@ -21,12 +24,14 @@ class MaintainProductsViewModel { ObservableList products = new ObservableList(new ArrayList()) - Product selectedProduct + @Bindable Optional selectedProduct - private final ProductsResourcesService productsResourcesService + final ProductsResourcesService productsResourcesService + EventEmitter productUpdate - MaintainProductsViewModel(ProductsResourcesService productsResourcesService) { + MaintainProductsViewModel(ProductsResourcesService productsResourcesService, EventEmitter productUpdate) { this.productsResourcesService = productsResourcesService + this.productUpdate = productUpdate fetchProducts() subscribe() } @@ -37,7 +42,13 @@ 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/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 96ed1c404..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 @@ -16,6 +17,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 +29,8 @@ import life.qbic.datamodel.dtos.business.services.ProductUnit */ class CreateProductView extends HorizontalLayout{ - private final CreateProductViewModel createProductViewModel + protected final CreateProductViewModel viewModel + protected final MaintainProductsController controller TextField productNameField TextField productDescriptionField @@ -35,13 +38,16 @@ class CreateProductView extends HorizontalLayout{ ComboBox productUnitComboBox ComboBox productCategoryComboBox + Button abortButton Button createProductButton - Button abortButton + Registration createProductButtonRegistration + Label titleLabel - CreateProductView(CreateProductViewModel createProductViewModel){ + CreateProductView(CreateProductViewModel createProductViewModel, MaintainProductsController controller){ + this.controller = controller + this.viewModel = createProductViewModel - this.createProductViewModel = createProductViewModel initTextFields() initComboBoxes() initButtons() @@ -52,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) @@ -113,29 +119,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 +150,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 +162,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 +216,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 } }) } @@ -263,22 +269,27 @@ class CreateProductView extends HorizontalLayout{ * It relies on the separate fields for validation. * @return */ - private boolean allValuesValid() { - return createProductViewModel.productNameValid \ - && createProductViewModel.productDescriptionValid \ - && createProductViewModel.productUnitValid \ - && createProductViewModel.productUnitPriceValid \ - && createProductViewModel.productCategoryValid + protected boolean allValuesValid() { + return viewModel.productNameValid \ + && viewModel.productDescriptionValid \ + && viewModel.productUnitValid \ + && viewModel.productUnitPriceValid \ + && viewModel.productCategoryValid } private void setupListeners(){ - abortButton.addClickListener({ clearAllFields() }) + + abortButton.addClickListener({clearAllFields() }) + createProductButtonRegistration = this.createProductButton.addClickListener({ + controller.createNewProduct(viewModel.productCategory, viewModel.productDescription,viewModel.productName, Double.parseDouble(viewModel.productUnitPrice),viewModel.productUnit) + }) + } /** * 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() @@ -286,11 +297,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/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/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/OfferOverview.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/offers/OfferOverview.groovy index 98ca7aa27..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,6 +1,8 @@ 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 /** * This class holds data for an offer overview @@ -11,10 +13,15 @@ import life.qbic.datamodel.dtos.business.OfferId * * @since 1.0.0 */ +@EqualsAndHashCode 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 +32,9 @@ class OfferOverview { final OfferId offerId + final Optional associatedProject + + @Deprecated OfferOverview( OfferId offerId, Date modificationDate, @@ -38,5 +48,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-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/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/products/ProductsDbConnector.groovy b/offer-manager-app/src/main/groovy/life/qbic/portal/offermanager/dataresources/products/ProductsDbConnector.groovy index 8e0534e50..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,14 +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 @@ -24,10 +27,13 @@ import java.sql.SQLException * @since 1.0.0 */ @Log4j2 -class ProductsDbConnector implements ArchiveProductDataSource, CreateProductDataSource { +class ProductsDbConnector implements ArchiveProductDataSource, CreateProductDataSource, CopyProductDataSource { 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. * @@ -55,77 +61,75 @@ 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 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 } 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 productCategory = row.category - String productId = row.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 - switch(productCategory) { - 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)) - 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)) - 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)) - 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)) - 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)) - break - } - if(product == null) { - log.error("Product could not be parsed from database query.") - log.error(row) - throw new DatabaseQueryException("Cannot parse product") - } else { - return 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 } - 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(?,?,?)" @@ -176,13 +180,16 @@ 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) { + 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] - return identifier + return Long.parseLong(identifier) } @@ -192,12 +199,14 @@ 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){ - 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' + private static String getProductType(Product product){ + 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 } @@ -215,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 @@ -251,32 +265,36 @@ 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()) { - 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 } /** - * 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)) @@ -284,10 +302,42 @@ 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) + + if(id) latestUniqueId = Long.parseLong(id.split('_')[1]) + } + } + + return latestUniqueId + 1 } /** @@ -308,7 +358,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/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..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,7 +1,8 @@ package life.qbic.portal.offermanager.dataresources.projects +import groovy.transform.CompileStatic 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.* @@ -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 @@ -25,166 +27,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") } - } - return -1 + 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 } - 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..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,6 +14,8 @@ 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 @@ -21,13 +23,25 @@ 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 /** @@ -37,147 +51,194 @@ 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) + } + + 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) + + space.setDescription(description) + + IOperation operation = new CreateSpacesOperation(space) + handleOperations(operation) } - } - - /** - * 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 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) } - } - private void createOpenbisSpace(String spaceName, String description) { - SpaceCreation space = new SpaceCreation() - space.setCode(spaceName) + 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) - space.setDescription(description) + Map properties = new HashMap<>() + properties.put("Q_EXPERIMENTAL_SETUP", emptyStudyXML) - IOperation operation = new CreateSpacesOperation(space) - handleOperations(operation) - } + createOpenbisExperiment(spaceCode, projectCode, experimentCode, "Q_PROJECT_DETAILS", properties) - 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); + 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) +} - IOperation operation = new CreateProjectsOperation(project); - 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)) - /** - * Returns a copied list of existing projects fetched upon creation of this class - */ - public List fetchProjects() { - return new ArrayList(openbisProjects); - } + sampleCreation.setExperimentId(new ExperimentIdentifier(experimentIdentifier)) + sampleCreation.setCode(sampleCode) - @Override + sampleCreation.setProperties(properties) + + IOperation operation = new CreateSamplesOperation(sampleCreation) + handleOperations(operation) +} + + /** + * Returns a copied list of existing projects fetched upon creation of this class + */ + List fetchProjects() { + return new ArrayList(openbisProjects) + } + + @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) + setupEmptyExperimentalDesign(space, projectCode) + 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 + } } - } } 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 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-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/pom.xml b/offer-manager-domain/pom.xml index 121a14f41..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.4 + 1.0.0-alpha.5 @@ -23,7 +23,7 @@ log4j 1.2.17 - + 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..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 @@ -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() + } } /** @@ -219,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 } } @@ -252,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 } } @@ -281,7 +303,10 @@ 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){ + // No overheads are assigned for data storage and project management + } + else { listOverheadProductItem.add(it) } } @@ -297,7 +322,7 @@ class Offer { List listNoOverheadProductItem = [] items.each { if (it.product instanceof DataStorage || it.product instanceof ProjectManagement) { - listNoOverheadProductItem.add(it) + listNoOverheadProductItem.add(it) } } return listNoOverheadProductItem @@ -343,6 +368,10 @@ class Offer { return overheadRatio } + Optional getAssociatedProject() { + return associatedProject + } + /** * Returns a deep copy of all available offer versions. * 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..359618b91 --- /dev/null +++ b/offer-manager-domain/src/main/groovy/life/qbic/business/products/Converter.groovy @@ -0,0 +1,117 @@ +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 +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 { + + private static final Logging log = Logger.getLogger(this.class) + + /** + * 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){ + long runningNumber = 0 //todo it should be possible to create products without a running number + 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, long runningNumber){ + Product product = null + switch (category) { + case ProductCategory.DATA_STORAGE: + product = new DataStorage(name, description, unitPrice,unit, runningNumber.toString()) + break + case ProductCategory.PRIMARY_BIOINFO: + product = new PrimaryAnalysis(name, description, unitPrice,unit, runningNumber.toString()) + break + case ProductCategory.PROJECT_MANAGEMENT: + product = new ProjectManagement(name, description, unitPrice,unit, runningNumber.toString()) + break + case ProductCategory.SECONDARY_BIOINFO: + product = new SecondaryAnalysis(name, description, unitPrice,unit, runningNumber.toString()) + break + case ProductCategory.SEQUENCING: + product = new Sequencing(name, description, unitPrice,unit, runningNumber.toString()) + break + case ProductCategory.PROTEOMIC: + product = new ProteomicAnalysis(name, description, unitPrice,unit, runningNumber.toString()) + break + 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 + } + + /** + * 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 f16f4682c..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,8 +1,12 @@ 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 +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 /** @@ -12,7 +16,6 @@ import life.qbic.datamodel.dtos.business.services.Product *

* * @since: 1.0.0 - * */ class CreateProduct implements CreateProductInput { @@ -28,14 +31,23 @@ 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 existed.", 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 6ce0fa435..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 @@ -14,9 +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 -} \ No newline at end of file + ProductId store(Product product) throws DatabaseQueryException, ProductExistsException + +} 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..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 @@ -2,9 +2,9 @@ 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.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 +15,16 @@ import spock.lang.Specification * @since 1.0.0 */ class CreateProductSpec extends Specification { - CreateProductOutput output - ProductId productId - Product product - - 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) - } + CreateProductOutput output = Mock(CreateProductOutput) + 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.store(product) >> { createdProductId } + and: "an instance of the use case" CreateProduct createProduct = new CreateProduct(dataSource, output) @@ -37,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"() { @@ -50,9 +62,9 @@ class CreateProductSpec extends Specification { String dataStatus = "" dataSource.store(product) >> { dataStatus = "not stored" - println(dataStatus) - throw new ProductExistsException(productId) + throw new ProductExistsException(createdProductId) } + and: "an instance of the use case" CreateProduct createProduct = new CreateProduct(dataSource, output) @@ -74,6 +86,7 @@ class CreateProductSpec extends Specification { dataSource.store(product) >> { dataStatus = "not stored" throw new DatabaseQueryException("This is a test") } + and: "an instance of the use case" CreateProduct createProduct = new CreateProduct(dataSource, output) @@ -87,4 +100,5 @@ class CreateProductSpec extends Specification { and: "the data was stored" dataStatus == "not stored" } + } diff --git a/pom.xml b/pom.xml index e2fa503a9..51df5d8cd 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 life.qbic The new offer manager http://github.com/qbicsoftware/qOffer_2.0 @@ -119,7 +119,7 @@ life.qbic data-model-lib - 2.3.0 + 2.5.0-SNAPSHOT com.vaadin diff --git a/qube.cfg b/qube.cfg index 11a3da7f3..fe4a5ec55 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 [bumpversion_files_whitelisted] dot_qube = .qube.yml