diff --git a/.qube.yml b/.qube.yml index aa10efe0db..1645f7c0a7 100644 --- a/.qube.yml +++ b/.qube.yml @@ -4,7 +4,7 @@ email: sven.fillinger@qbic.uni-tuebingen.de project_name: data-model-lib project_short_description: "Data models. A collection of QBiC's central data models\ \ and DTOs. " -version: 2.2.0 +version: 2.3.0 domain: lib language: groovy project_slug: data-model-lib diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b24e23a594..34e4583df5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,30 @@ Changelog This project adheres to `Semantic Versioning `_. +2.3.0 (2021-03-16) +------------------ + +**Added** + +* overheadRatio property for ``life.qbic.datamodel.dtos.business.Offer`` + +* ``life.qbic.datamodel.dtos.projectmanagement.ProjectIdentifier``, ``life.qbic.datamodel.dtos.projectmanagement.ProjectCode``, ``life.qbic.datamodel.dtos.projectmanagement.ProjectSpace`` and ``life.qbic.datamodel.dtos.projectmanagement.Project`` to describe QBiC projects + +* ``life.qbic.datamodel.dtos.business.ProjectApplication`` to describe a project application for registration at QBiC's data management platform + +* Added uniqueId field to ``life.qbic.datamodel.dtos.business.ProductId`` (`#173 `_) + +* Add `Hour` ``life.qbic.datamodel.dtos.business.services.ProductUnit.PER_HOUR`` (`#175 `_) + +**Fixed** + +**Dependencies** + +**Deprecated** + +* ``life.qbic.datamodel.dtos.business.ProductId#identifier`` is replaced by ``life.qbic.datamodel.dtos.business.ProductId#uniqueId`` (`#173 `_) + + 2.2.0 (2021-03-02) ------------------ @@ -27,7 +51,7 @@ This project adheres to `Semantic Versioning `_. * Introduce a schema resource for bioinformatic pipeline result sets validation via ``life.qbic.datamodel.pipelines.PipelineOutput`` (`#159 `_) * Add field ``life.qbic.datamodel.dtos.business.Offer#projectObjective``, will replace ``life.qbic.datamodel.dtos.business.Offer#projectDescription`` (`#161 `_) * Add fields ``life.qbic.datamodel.dtos.business.Offer#itemsWithOverhead``, ``life.qbic.datamodel.dtos.business.Offer#itemsWithoutOverhead``, -``life.qbic.datamodel.dtos.business.Offer#itemsWithOverheadNetPrice`` and ``life.qbic.datamodel.dtos.business.Offer#itemsWithoutOverheadNetPrice`` to Offer DTO (`#160 `_) + ``life.qbic.datamodel.dtos.business.Offer#itemsWithOverheadNetPrice`` and ``life.qbic.datamodel.dtos.business.Offer#itemsWithoutOverheadNetPrice`` to Offer DTO (`#160 `_) **Fixed** @@ -324,4 +348,3 @@ This project adheres to `Semantic Versioning `_. **Dependencies** **Deprecated** - diff --git a/docs/conf.py b/docs/conf.py index bec81cf81b..70807a0db4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,9 +55,9 @@ # the built documents. # # The short X.Y version. -version = '2.2.0' +version = '2.3.0' # The full version, including alpha/beta/rc tags. -release = '2.2.0' +release = '2.3.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/pom.xml b/pom.xml index 2310284a31..bd3c867622 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ life.qbic data-model-lib - 2.2.0 + 2.3.0 data-model-lib http://github.com/qbicsoftware/data-model-lib Data models. A collection of QBiC's central data models and DTOs. diff --git a/qube.cfg b/qube.cfg index 9a18fa84cc..929a2b7b02 100644 --- a/qube.cfg +++ b/qube.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 2.2.0 +current_version = 2.3.0 [bumpversion_files_whitelisted] dot_qube = .qube.yml diff --git a/src/main/groovy/life/qbic/datamodel/dtos/business/Offer.groovy b/src/main/groovy/life/qbic/datamodel/dtos/business/Offer.groovy index 59b0b81fad..dff3019983 100644 --- a/src/main/groovy/life/qbic/datamodel/dtos/business/Offer.groovy +++ b/src/main/groovy/life/qbic/datamodel/dtos/business/Offer.groovy @@ -91,6 +91,10 @@ class Offer { * The net price of all items for which an overhead cost is not applicable, without overhead and taxes */ final double itemsWithoutOverheadNetPrice + /** + * The overhead ratio applied to the pricing dependent on the customer affiliation + */ + final double overheadRatio static class Builder { @@ -122,7 +126,7 @@ class Offer { double overheads double itemsWithOverheadNet double itemsWithoutOverheadNet - + double overheadRatio /** * @deprecated Replaced with {@link #projectObjective}, since 2.1.0 */ @@ -151,6 +155,7 @@ class Offer { this.itemsWithOverheadNet = 0 this.itemsWithoutOverheadNet = 0 this.checksum = "" + this.overheadRatio = 0 /* Deprecated @@ -223,6 +228,11 @@ class Offer { return this } + Builder overheadRatio(double overheadRatio){ + this.overheadRatio = overheadRatio + return this + } + Offer build() { return new Offer(this) } @@ -258,6 +268,7 @@ class Offer { this.itemsWithoutOverhead = builder.itemsWithoutOverhead this.itemsWithOverheadNetPrice = builder.itemsWithOverheadNet this.itemsWithoutOverheadNetPrice = builder.itemsWithoutOverheadNet + this.overheadRatio = builder.overheadRatio /* Deprecated properties diff --git a/src/main/groovy/life/qbic/datamodel/dtos/business/ProductId.groovy b/src/main/groovy/life/qbic/datamodel/dtos/business/ProductId.groovy index 0bf3c417f4..fbf17f95e3 100644 --- a/src/main/groovy/life/qbic/datamodel/dtos/business/ProductId.groovy +++ b/src/main/groovy/life/qbic/datamodel/dtos/business/ProductId.groovy @@ -23,27 +23,88 @@ class ProductId { /** * Identifying number used in conjunction with the type */ - private final String identifier + private final long uniqueId + + /** + * A builder for ProductId instances. + */ + static class Builder { + private String productType + private long uniqueId + + /** + * + * @param productType + * @param identifier the unique id - will be interpreted as unsigned long + */ + Builder(String productType, String identifier) { + this.productType = Objects.requireNonNull(productType) + this.uniqueId = Objects.requireNonNull(Long.parseUnsignedLong(identifier)) + } + + /** + * + * @param productType + * @param uniqueId the unique id - will be interpreted as unsigned long + */ + Builder(String productType, long uniqueId) { + this.productType = Objects.requireNonNull(productType) + this.uniqueId = Objects.requireNonNull(uniqueId) + } + + Builder productType(String productType) { + this.productType = productType + return this + } + + Builder uniqueId(long identifier) { + this.uniqueId = identifier + return this + } + /** + * Constructs a product identifier based on the configuration of the builder + * @return + */ + ProductId build() { + return new ProductId(this) + } + } /** * Creates an identifier object with the * * @param type describing the type of the underlying identifier * @param identifier describes the identifying running number + * @deprecated please use {@link ProductId.Builder} */ - + @Deprecated ProductId(String type, String identifier){ - this.type = Objects.requireNonNull(type, "type must not be null") - this.identifier= Objects.requireNonNull(identifier, "version must not be null") + Builder builder = new Builder(type, identifier) + this.type = builder.productType + this.uniqueId = builder.uniqueId + } + private ProductId(Builder builder) { + this.type = builder.productType + this.uniqueId = builder.uniqueId } /** * Returns the identifying running number - * @return + * @return the identifier + * @deprecated please use {@link ProductId#getUniqueId()} */ + @Deprecated String getIdentifier() { - return identifier + return uniqueId.toString() + } + + /** + * Returns the identifying running number + * @return the identifier + */ + Long getUniqueId() { + return uniqueId } /** * Returns the type of the identifier @@ -62,6 +123,6 @@ class ProductId { */ @Override String toString() { - return type + "_" + identifier + return "${type}_${uniqueId}" } } \ No newline at end of file diff --git a/src/main/groovy/life/qbic/datamodel/dtos/business/ProjectApplication.groovy b/src/main/groovy/life/qbic/datamodel/dtos/business/ProjectApplication.groovy new file mode 100644 index 0000000000..1e833681c4 --- /dev/null +++ b/src/main/groovy/life/qbic/datamodel/dtos/business/ProjectApplication.groovy @@ -0,0 +1,81 @@ +package life.qbic.datamodel.dtos.business + +import life.qbic.datamodel.dtos.business.Customer +import life.qbic.datamodel.dtos.business.OfferId +import life.qbic.datamodel.dtos.business.ProjectManager +import life.qbic.datamodel.dtos.projectmanagement.ProjectCode +import life.qbic.datamodel.dtos.projectmanagement.ProjectSpace + +/** + * Information about a desired project registration in QBiC's project management system. + * + * Instances of this class describe a request to register a new project at QBiC. + * + * Beside the mandatory fields required to be passed to the constructor, there are two optional + * fields: + * 1: projectSpace + * 2: projectCode + * If any of these are not defined, then either of it will be created randomly. + * + * @since 2.3.0 + */ +class ProjectApplication { + + /** + * The associated offer + */ + final OfferId linkedOffer + + /** + * A short and precise project title + */ + final String projectTitle + + /** + * A descriptive project objective + */ + final String projectObjective + + /** + * A description about the experimental design + */ + final String experimentalDesign + + /** + * The associated project manager + */ + final ProjectManager projectManager + + /** + * The associated customer + */ + final Customer customer + + /** + * The requested project space the project shall be associated with + */ + final ProjectSpace projectSpace + + /** + * The desired project code + */ + final ProjectCode projectCode + + ProjectApplication(OfferId linkedOffer, + String projectTitle, + String projectObjective, + String experimentalDesign, + ProjectManager projectManager, + ProjectSpace projectSpace, + Customer customer, + ProjectCode code) { + this.linkedOffer = Objects.requireNonNull(linkedOffer) + this.projectTitle = Objects.requireNonNull(projectTitle) + this.projectObjective = Objects.requireNonNull(projectObjective) + this.experimentalDesign = Objects.requireNonNull(experimentalDesign) + this.projectManager = Objects.requireNonNull(projectManager) + this.customer = Objects.requireNonNull(customer) + this.projectSpace = Objects.requireNonNull(projectSpace) + this.projectCode = Objects.requireNonNull(code) + } +} diff --git a/src/main/groovy/life/qbic/datamodel/dtos/business/services/ProductUnit.groovy b/src/main/groovy/life/qbic/datamodel/dtos/business/services/ProductUnit.groovy index e0265c0693..4c38252f7b 100644 --- a/src/main/groovy/life/qbic/datamodel/dtos/business/services/ProductUnit.groovy +++ b/src/main/groovy/life/qbic/datamodel/dtos/business/services/ProductUnit.groovy @@ -9,7 +9,8 @@ enum ProductUnit { PER_GIGABYTE("Gigabyte"), PER_SAMPLE("Sample"), - PER_DATASET("Dataset") + PER_DATASET("Dataset"), + PER_HOUR("Hour") /** Holds the String value of the enum diff --git a/src/main/groovy/life/qbic/datamodel/dtos/projectmanagement/Project.groovy b/src/main/groovy/life/qbic/datamodel/dtos/projectmanagement/Project.groovy new file mode 100644 index 0000000000..38470b2d15 --- /dev/null +++ b/src/main/groovy/life/qbic/datamodel/dtos/projectmanagement/Project.groovy @@ -0,0 +1,66 @@ +package life.qbic.datamodel.dtos.projectmanagement + +import life.qbic.datamodel.dtos.business.OfferId + +/** + * A typical scientific QBiC project. + * + * Describes and provides information about a scientific project + * at QBiC. + * + * @since 2.3.0 + */ +class Project { + + /** + * A short but descriptive project title + */ + final String projectTitle + + /** + * QBiC's internal project identifier + */ + final ProjectIdentifier projectId + + /** + * The associated offer + */ + final OfferId linkedOffer + + private Project(Builder builder) { + this.projectId = Objects.requireNonNull(builder.projectIdentifier) + this.projectTitle = Objects.requireNonNull(builder.projectTitle) + this.linkedOffer = builder.linkedOfferId + } + + static class Builder { + private ProjectIdentifier projectIdentifier + private String projectTitle + private OfferId linkedOfferId + + Builder(ProjectIdentifier projectIdentifier, String projectTitle) { + this.projectIdentifier = projectIdentifier + this.projectTitle = projectTitle + this.linkedOfferId = null + } + + Builder projectIdentifier(ProjectIdentifier projectIdentifier) { + this.projectIdentifier = projectIdentifier + return this + } + + Builder projectTitle(String projectTitle) { + this.projectTitle = projectTitle + return this + } + + Builder linkedOfferId(OfferId offerId) { + this.linkedOfferId = offerId + return this + } + + Project build() { + return new Project(this) + } + } +} diff --git a/src/main/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectCode.groovy b/src/main/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectCode.groovy new file mode 100644 index 0000000000..34d93efa8f --- /dev/null +++ b/src/main/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectCode.groovy @@ -0,0 +1,51 @@ +package life.qbic.datamodel.dtos.projectmanagement + +/** + * Describes the project code, that identifies the project within a project space. + * + * A project code is the unique identifier of a QBiC project within a project space. + * + * A project code can be validated with the following regular expression: "Q[A-X0-9]{4}". + * + * @since 2.3.0 + */ +class ProjectCode { + + final String code + + private static final def REGEX = ~'Q[A-X0-9]{4}' + + /** + * Constructs a project code instance based on the given code string. + * @param code The project code + * @throws IllegalArgumentException If the code parameter does not match format requirements. + */ + ProjectCode(String code) throws IllegalArgumentException { + Objects.requireNonNull(code, "Code must not be null") + this.code = code.trim() + validateCode() + } + + private void validateCode() { + if(! REGEX.matcher(code).matches()) { + throw new IllegalArgumentException("${code} is not a valid project code.") + } + } + + @Override + String toString() { + return this.code + } + + @Override + boolean equals(Object obj) { + if (this.is(obj)) { + return true + } + if (!obj instanceof ProjectCode) { + return false + } + ProjectCode otherCode = (ProjectCode) obj + return this.code.equals(otherCode.code) + } +} diff --git a/src/main/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectIdentifier.groovy b/src/main/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectIdentifier.groovy new file mode 100644 index 0000000000..25333d09e6 --- /dev/null +++ b/src/main/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectIdentifier.groovy @@ -0,0 +1,36 @@ +package life.qbic.datamodel.dtos.projectmanagement + +/** + * Global project identifier for QBiC projects + * + * Identifies a project by its project space and project code. + * + * @since 2.3.0 + */ +class ProjectIdentifier { + + /** + * The associated project space + */ + final ProjectSpace projectSpace + + /** + * The associated project code within the project space + */ + final ProjectCode projectCode + + /** + * Constructor for a project identifier. + * @param projectSpace The project space the project is associated with + * @param projectCode The project code, identifying the project within the project space + */ + ProjectIdentifier(ProjectSpace projectSpace, ProjectCode projectCode) { + this.projectSpace = Objects.requireNonNull(projectSpace) + this.projectCode = Objects.requireNonNull(projectCode) + } + + @Override + String toString() { + return "${projectSpace.toString()}/${projectCode.toString()}" + } +} diff --git a/src/main/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectSpace.groovy b/src/main/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectSpace.groovy new file mode 100644 index 0000000000..f2383ba46b --- /dev/null +++ b/src/main/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectSpace.groovy @@ -0,0 +1,62 @@ +package life.qbic.datamodel.dtos.projectmanagement + +/** + *

Represents a QBiC project space

+ * + *

A space is a logical grouping of projects that have the same context.

+ * + *

A project space has a name that follows this convention:

+ * + *
    + *
  • No white space
  • + *
  • Capital letters only
  • + *
  • Inner white space is replaced by `_`
  • + *
  • Trailing and leading white space are trimmed
  • + *
  • Dashes are replaced by `_`
  • + *
+ * @since 2.3.0 + */ +final class ProjectSpace { + + /** + * The project space name + */ + final String name + + ProjectSpace(String name) { + final def space = Objects.requireNonNull(name, "Space name must not be null.") + this.name = formatSpaceName(space) + } + + private static String formatSpaceName(String name) { + def capitalizedName = name.trim().toUpperCase() + def refactoredName = capitalizedName.replaceAll("\\s+", "_") + .replaceAll("-", "_") + return refactoredName + } + + /** + * Returns a string representation of the space name following the format conventions stated + * in the class description. + * + * Example: "my example space " -> "MY_EXAMPLE_SPACE" + * + * @return The project space name + */ + @Override + String toString() { + return this.name + } + + @Override + boolean equals(Object obj) { + if (this.is(obj)) { + return true + } + if (!obj instanceof ProjectSpace) { + return false + } + ProjectSpace otherSpace = (ProjectSpace) obj + return this.name.equals(otherSpace.name) + } +} diff --git a/src/test/groovy/life/qbic/datamodel/dtos/business/OfferSpec.groovy b/src/test/groovy/life/qbic/datamodel/dtos/business/OfferSpec.groovy index d0b1d693d0..8c797d0be2 100644 --- a/src/test/groovy/life/qbic/datamodel/dtos/business/OfferSpec.groovy +++ b/src/test/groovy/life/qbic/datamodel/dtos/business/OfferSpec.groovy @@ -33,10 +33,11 @@ class OfferSpec extends Specification { ProductItem item = new ProductItem(2,new Sequencing("DNA Sequencing","This is a sequencing package",1.50, ProductUnit.PER_SAMPLE, "1")) double itemsWithOverheadNet = 123 double itemsWithoutOverheadNet = 456 + double overheadRatio = 0.2 when: Offer testOffer = new Offer.Builder(customer, projectManager, "Archer", "Cartoon Series", selectedAffiliation) - .modificationDate(date).expirationDate(date).totalPrice(price).identifier(offerId).taxes(vat).overheads(overhead).netPrice(net).items([item]).itemsWithOverhead([item]).itemsWithoutOverhead([item]).itemsWithOverheadNet(itemsWithOverheadNet).itemsWithoutOverheadNet(itemsWithoutOverheadNet) + .modificationDate(date).expirationDate(date).totalPrice(price).identifier(offerId).taxes(vat).overheads(overhead).netPrice(net).items([item]).itemsWithOverhead([item]).itemsWithoutOverhead([item]).itemsWithOverheadNet(itemsWithOverheadNet).itemsWithoutOverheadNet(itemsWithoutOverheadNet).overheadRatio(overheadRatio) .build() then: @@ -57,6 +58,7 @@ class OfferSpec extends Specification { testOffer.getItemsWithoutOverhead() == [item] testOffer.getItemsWithOverheadNetPrice() == 123 testOffer.getItemsWithoutOverheadNetPrice() == 456 + testOffer.overheadRatio == 0.2 } def "Missing optional Field definitions shall haven null values in an Offer object"() { diff --git a/src/test/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectCodeSpec.groovy b/src/test/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectCodeSpec.groovy new file mode 100644 index 0000000000..764af16413 --- /dev/null +++ b/src/test/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectCodeSpec.groovy @@ -0,0 +1,35 @@ +package life.qbic.datamodel.dtos.projectmanagement + +import spock.lang.Specification + +/** + * Tests the project code vaidation + * + * @since 2.3.0 + */ +class ProjectCodeSpec extends Specification { + + def "A String violating the project code format standard shall throw an IllegalArgumentException" () { + given: + String invalidCode = " M-ABCD " + + when: + new ProjectCode(invalidCode) + + then: + thrown(IllegalArgumentException) + + } + + def "A String sticking to the project code format standard shall create a ProjectCode object instance successfully"() { + given: + String validCode = " QABCD " + + when: + ProjectCode projectCode = new ProjectCode(validCode) + + then: + noExceptionThrown() + assert projectCode.code.equals("QABCD") + } +} diff --git a/src/test/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectSpaceSpec.groovy b/src/test/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectSpaceSpec.groovy new file mode 100644 index 0000000000..a10f8a88b7 --- /dev/null +++ b/src/test/groovy/life/qbic/datamodel/dtos/projectmanagement/ProjectSpaceSpec.groovy @@ -0,0 +1,33 @@ +package life.qbic.datamodel.dtos.projectmanagement + +import spock.lang.Specification + +/** + * Tests the formatting of the project space + * + * @since 2.3.0 + */ +class ProjectSpaceSpec extends Specification { + + def "A string including inner white space will be replaced with underscore chars"() { + given: + String projectSpaceName = " my new space " + + when: + ProjectSpace space = new ProjectSpace(projectSpaceName) + + then: + space.name.equals("MY_NEW_SPACE") + } + + def "Dashes are replaced by underscores"() { + given: + String projectSpaceName = "my-new-space" + + when: + ProjectSpace space = new ProjectSpace(projectSpaceName) + + then: + space.name.equals("MY_NEW_SPACE") + } +}