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