diff --git a/.github/workflows/CI-full.yml b/.github/workflows/CI-full.yml index 8fed452f6a..ef37e52e37 100644 --- a/.github/workflows/CI-full.yml +++ b/.github/workflows/CI-full.yml @@ -6,11 +6,11 @@ on: types: [published] env: - python-version: "3.9" + python-version: "3.10" jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Get the VCell version from tags diff --git a/docker/build/Dockerfile-batch-dev b/docker/build/Dockerfile-batch-dev index 511fa5eced..06de06ba00 100644 --- a/docker/build/Dockerfile-batch-dev +++ b/docker/build/Dockerfile-batch-dev @@ -1,4 +1,4 @@ -FROM ghcr.io/virtualcell/vcell-solvers:v0.8.1.2 +FROM ghcr.io/virtualcell/vcell-solvers:v0.8.1.3 RUN apt-get -y update && \ apt-get install -y curl && \ diff --git a/docker/build/batch/JavaPostprocessor64 b/docker/build/batch/JavaPostprocessor64 index 798074d905..46c7d09461 100755 --- a/docker/build/batch/JavaPostprocessor64 +++ b/docker/build/batch/JavaPostprocessor64 @@ -41,13 +41,13 @@ jvmprop="${jvmprop} -Dvcell.mongodb.database=${mongodb_database}" jvmprop="${jvmprop} -Dvcell.mongodb.host.internal=${mongodbhost_internal}" jvmprop="${jvmprop} -Dvcell.mongodb.port.internal=${mongodbport_internal}" -jvmoptions="-XX:MaxRAMPercentage=100 -Xms64M -Xmx${java_mem_Xmx} -XX:+PrintFlagsFinal -XshowSettings:vm" +#jvmoptions="-XX:MaxRAMPercentage=100 -Xms64M -Xmx${java_mem_Xmx} -XX:+PrintFlagsFinal -XshowSettings:vm" +jvmoptions="-XX:MaxRAMPercentage=100 -Xms64M -Xmx${java_mem_Xmx}" arguments=$* echo "starting postprocessor" -echo "expecting jre 1.8_144 or later for memory options" java -version java ${jvmoptions} ${jvmprop} ${postprocessor_mainclass} ${arguments} diff --git a/docker/build/batch/JavaPreprocessor64 b/docker/build/batch/JavaPreprocessor64 index 3e6a60dc4b..73fa3bed57 100755 --- a/docker/build/batch/JavaPreprocessor64 +++ b/docker/build/batch/JavaPreprocessor64 @@ -41,13 +41,13 @@ jvmprop="${jvmprop} -Dvcell.mongodb.database=${mongodb_database}" jvmprop="${jvmprop} -Dvcell.mongodb.host.internal=${mongodbhost_internal}" jvmprop="${jvmprop} -Dvcell.mongodb.port.internal=${mongodbport_internal}" -jvmoptions="-XX:MaxRAMPercentage=80 -Xms64M -Xmx${java_mem_Xmx} -XX:+PrintFlagsFinal -XshowSettings:vm" +#jvmoptions="-XX:MaxRAMPercentage=80 -Xms64M -Xmx${java_mem_Xmx} -XX:+PrintFlagsFinal -XshowSettings:vm" +jvmoptions="-XX:MaxRAMPercentage=80 -Xms64M -Xmx${java_mem_Xmx}" arguments=$* echo "starting preprocessor" -echo "expecting jre 1.8_144 or later for memory options" java -version java ${jvmoptions} ${jvmprop} ${preprocessor_mainclass} ${arguments} diff --git a/docker/build/batch/JavaSimExe64 b/docker/build/batch/JavaSimExe64 index 67b3902e2f..3b5ebb6f19 100755 --- a/docker/build/batch/JavaSimExe64 +++ b/docker/build/batch/JavaSimExe64 @@ -37,11 +37,11 @@ jvmprop="${jvmprop} -Dvcell.mongodb.database=${mongodb_database}" jvmprop="${jvmprop} -Dvcell.mongodb.host.internal=${mongodbhost_internal}" jvmprop="${jvmprop} -Dvcell.mongodb.port.internal=${mongodbport_internal}" -jvmoptions="-XX:MaxRAMPercentage=80 -Xms64M -Xmx${java_mem_Xmx} -XX:+PrintFlagsFinal -XshowSettings:vm" +#jvmoptions="-XX:MaxRAMPercentage=80 -Xms64M -Xmx${java_mem_Xmx} -XX:+PrintFlagsFinal -XshowSettings:vm" +jvmoptions="-XX:MaxRAMPercentage=80 -Xms64M -Xmx${java_mem_Xmx}" echo "starting java solver" -echo "expecting jre 1.8_144 or later for memory options" java -version arguments=$* diff --git a/docker/build/build.sh b/docker/build/build.sh index 88ab80093d..a5c5b39872 100755 --- a/docker/build/build.sh +++ b/docker/build/build.sh @@ -230,20 +230,19 @@ build_mongo() { } build_batch_singularity() { + # turn on logging of commands + set -x if [ "$skip_singularity" == "false" ]; then if [ -x "$(command -v singularity)" ]; then build_batch_singularity_direct if [[ $? -ne 0 ]]; then echo "failed to build singularity image using singularity commands"; exit 1; fi else - if [ -x "$(command -v vagrant)" ]; then - build_batch_singularity_vagrant - if [[ $? -ne 0 ]]; then echo "failed to build singularity image using singularity vagrant box"; exit 1; fi - else - echo "found neither singularity nor vagrant, cannot build singularity image" - exit 1 - fi - fi + echo "singularity not found, cannot build singularity image" + exit 1 + fi fi + # turn off logging of commands + set +x } build_batch_singularity_direct() { @@ -301,87 +300,6 @@ EOF cd .. } -build_batch_singularity_vagrant() { - echo "" - cmd="cd singularity-vm" - cd singularity-vm - echo "" - echo "CURRENT DIRECTORY IS $PWD" - - # - # prepare Vagrant Singularity box for building the singularity image (bring up, install cert) - # - echo "" - echo "generating singularity image for vcell-batch and uploading to remote server for HTC cluster" - cmd="sudo scp $ssh_key vcell@vcell-docker.cam.uchc.edu:/usr/local/deploy/registry_certs/domain.cert ." - echo $cmd - ($cmd) || (echo "failed to download cert from vcell-docker private Docker registry") - - echo "" - echo "vagrant up" - vagrant up - if [[ $? -ne 0 ]]; then echo "failed to bring vagrant up"; fi - - echo "" - remote_cmd="sudo cp /vagrant/domain.cert /usr/local/share/ca-certificates/vcell-docker.cam.uchc.edu.crt" - echo "vagrant ssh -c \"$remote_cmd\"" - vagrant ssh -c "$remote_cmd" - if [[ $? -ne 0 ]]; then echo "failed to upload domain.cert to trust the private Docker registry" && exit 1; fi - - echo "" - remote_cmd="sudo update-ca-certificates" - echo "vagrant ssh -c \"$remote_cmd\"" - vagrant ssh -c "$remote_cmd" - if [[ $? -ne 0 ]]; then - echo "failed to update ca certificates in vagrant box" && exit 1 - fi - # - # create temporary Singularity file which imports existing docker image from registry and adds a custom entrypoint - # - _vcell_batch_docker_name="${repo}/vcell-batch:${tag}" - _singularity_image_file="${_vcell_batch_docker_name//[\/:]/_}.img" - _singularity_file="Singularity_${_vcell_batch_docker_name//[\/:]/_}" - -cat <$_singularity_file -Bootstrap: docker -From: $_vcell_batch_docker_name - -%runscript - - exec /vcellscripts/entrypoint.sh "\$@" - -%labels - -AUTHOR jcschaff -EOF - - echo "" - echo "wrote Singularity file $_singularity_file" - cat $_singularity_file - - # - # build the singularity image and place in singularity-vm directory - # - echo "" - remote_cmd="sudo singularity build /vagrant/$_singularity_image_file /vagrant/$_singularity_file" - echo "vagrant ssh -c \"$remote_cmd\"" - vagrant ssh -c "$remote_cmd" - if [[ $? -ne 0 ]]; then echo "failed to build singularity image from vagrant" && exit 1; fi - - # - # bring down Vagrant Singularity box - # - echo "" - echo "vagrant halt" - vagrant halt - if [[ $? -ne 0 ]]; then echo "failed to stop vagrant box"; fi - - echo "" - echo "created Singularity image for vcell-bash ./$local_singularity_image_name locally (in ./singularity-vm folder), can be pushed to remote server during deploy" - echo "" - echo "cd .." - cd .. -} build_opt_singularity() { if [ "$skip_singularity" == "false" ]; then @@ -389,15 +307,9 @@ build_opt_singularity() { build_opt_singularity_direct if [[ $? -ne 0 ]]; then echo "failed to build opt singularity image using singularity commands"; exit 1; fi else -# if [ -x "$(command -v vagrant)" ]; then -# build_opt_singularity_vagrant -# if [[ $? -ne 0 ]]; then echo "failed to build opt singularity image using singularity vagrant box"; exit 1; fi -# else -# echo "found neither singularity nor vagrant, cannot build opt singularity image" - echo "singularity not found, cannot build opt singularity image (vagrant build not implemented)" - exit 1 - fi -# fi + echo "singularity not found, cannot build opt singularity image" + exit 1 + fi fi } diff --git a/pythonCopasiOpt/Dockerfile b/pythonCopasiOpt/Dockerfile index 2a908f9c42..f88764e4f8 100644 --- a/pythonCopasiOpt/Dockerfile +++ b/pythonCopasiOpt/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-slim-buster +FROM python:3.10-slim-buster RUN apt -y update && apt -y upgrade && \ apt-get install -y curl diff --git a/pythonCopasiOpt/vcell-opt/poetry.lock b/pythonCopasiOpt/vcell-opt/poetry.lock index 8b00c8a3be..557c4ee14f 100644 --- a/pythonCopasiOpt/vcell-opt/poetry.lock +++ b/pythonCopasiOpt/vcell-opt/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "anyio" @@ -1523,7 +1523,11 @@ files = [ ] [package.dependencies] -numpy = {version = ">=1.20.3", markers = "python_version < \"3.10\""} +numpy = [ + {version = ">=1.20.3", markers = "python_version < \"3.10\""}, + {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, + {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, +] python-dateutil = ">=2.8.1" pytz = ">=2020.1" @@ -2491,5 +2495,5 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" -python-versions = "3.9.*" -content-hash = "cd637e67ad66110dd5d90d7a9a114972ae8a783cb4633adc5d7fbd3bf80a0b49" +python-versions = "^3.9" +content-hash = "421aec668be9be404488993f689d6eb93084d4b5bd306130fd79656dd2660513" diff --git a/pythonCopasiOpt/vcell-opt/pyproject.toml b/pythonCopasiOpt/vcell-opt/pyproject.toml index e23c8a4e5c..8601fc6291 100644 --- a/pythonCopasiOpt/vcell-opt/pyproject.toml +++ b/pythonCopasiOpt/vcell-opt/pyproject.toml @@ -6,7 +6,7 @@ authors = ["Jim Schaff "] readme = "README.md" [tool.poetry.dependencies] -python = "3.9.*" +python = "^3.9" copasi-basico = "0.40" python-copasi = "4.37.264" jupyter = "^1.0.0" diff --git a/pythonVtk/poetry.lock b/pythonVtk/poetry.lock index c99f918e3d..a8f59cb1f9 100644 --- a/pythonVtk/poetry.lock +++ b/pythonVtk/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "colorama" @@ -25,6 +25,7 @@ files = [ {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"}, {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"}, {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"}, + {file = "contourpy-1.1.0-cp310-cp310-win32.whl", hash = "sha256:9b2dd2ca3ac561aceef4c7c13ba654aaa404cf885b187427760d7f7d4c57cff8"}, {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"}, {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"}, {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"}, @@ -33,6 +34,7 @@ files = [ {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"}, {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"}, {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"}, + {file = "contourpy-1.1.0-cp311-cp311-win32.whl", hash = "sha256:edb989d31065b1acef3828a3688f88b2abb799a7db891c9e282df5ec7e46221b"}, {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"}, {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"}, {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"}, @@ -41,6 +43,7 @@ files = [ {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"}, {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"}, {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"}, + {file = "contourpy-1.1.0-cp38-cp38-win32.whl", hash = "sha256:108dfb5b3e731046a96c60bdc46a1a0ebee0760418951abecbe0fc07b5b93b27"}, {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"}, {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"}, {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"}, @@ -49,6 +52,7 @@ files = [ {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"}, {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"}, {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"}, + {file = "contourpy-1.1.0-cp39-cp39-win32.whl", hash = "sha256:71551f9520f008b2950bef5f16b0e3587506ef4f23c734b71ffb7b89f8721999"}, {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"}, {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"}, {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"}, @@ -667,5 +671,5 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" -python-versions = "3.9.*" -content-hash = "a0ad347fc29984fdf3686a9b3a32f49a719b3a1135bf1e3399c38fb65c91ea82" +python-versions = "^3.9" +content-hash = "007ea72bba6c933e5af6a4466fb8e46e2d776c9717434086a47bf2022094bb33" diff --git a/pythonVtk/pyproject.toml b/pythonVtk/pyproject.toml index e73fbbe812..26ae66167c 100644 --- a/pythonVtk/pyproject.toml +++ b/pythonVtk/pyproject.toml @@ -6,7 +6,7 @@ authors = ["Jim Schaff "] readme = "README.md" [tool.poetry.dependencies] -python = "3.9.*" +python = "^3.9" vtk = "^9.2.6" thrift = "^0.16.0" diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java index 59a2fe642a..8768d3a7a9 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java @@ -387,7 +387,7 @@ private int getSelectedParamScanJobIndex(){ } int selectedJobIndex = -1; try { - selectedJobIndex = BeanUtils.coordinateToIndex(indices, bounds); + selectedJobIndex = BeanUtils.parameterScanCoordinateToJobIndex(indices, bounds); } catch (RuntimeException exc) { exc.printStackTrace(); } diff --git a/vcell-client/src/main/java/cbit/vcell/client/desktop/biomodel/AnnotationsPanel.java b/vcell-client/src/main/java/cbit/vcell/client/desktop/biomodel/AnnotationsPanel.java index 88c39fd626..445d736582 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/desktop/biomodel/AnnotationsPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/client/desktop/biomodel/AnnotationsPanel.java @@ -1116,6 +1116,9 @@ private boolean isEqual(MiriamResource aThis, MiriamResource aThat) { @SuppressWarnings("unchecked") private void initializeComboBoxURI() { // jComboBoxURI = getJComboBoxURI(); + if (selectedObject == null && vcMetaData == null){ + return; + } Identifiable entity = getIdentifiable(selectedObject); defaultComboBoxModelURI.removeAllElements(); List tooltips = new ArrayList<> (); diff --git a/vcell-core/src/main/java/cbit/vcell/solver/MathOverrides.java b/vcell-core/src/main/java/cbit/vcell/solver/MathOverrides.java index 0e837b0b27..27e59a57d8 100644 --- a/vcell-core/src/main/java/cbit/vcell/solver/MathOverrides.java +++ b/vcell-core/src/main/java/cbit/vcell/solver/MathOverrides.java @@ -14,7 +14,6 @@ import cbit.vcell.math.*; import cbit.vcell.model.common.VCellErrorMessages; import cbit.vcell.parser.*; -import jscl.math.function.Exp; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.vcell.util.*; @@ -22,7 +21,6 @@ import org.vcell.util.IssueContext.ContextType; import java.util.*; -import java.util.function.BiPredicate; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -353,7 +351,7 @@ public Expression getActualExpression(String key, int index){ for(int i = 0; i < names.length; i++){ bounds[i] = getConstantArraySpec(names[i]).getNumValues() - 1; } - int[] coordinates = BeanUtils.indexToCoordinate(index, bounds); + int[] coordinates = BeanUtils.jobIndexToScanParameterCoordinate(index, bounds); int localIndex = coordinates[java.util.Arrays.binarySearch(names, key)]; return getConstantArraySpec(key).getConstants()[localIndex].getExpression(); } diff --git a/vcell-rest/src/main/java/org/vcell/restq/handlers/BioModelResource.java b/vcell-rest/src/main/java/org/vcell/restq/handlers/BioModelResource.java index b49ec94ce0..cf70b1da66 100644 --- a/vcell-rest/src/main/java/org/vcell/restq/handlers/BioModelResource.java +++ b/vcell-rest/src/main/java/org/vcell/restq/handlers/BioModelResource.java @@ -9,11 +9,14 @@ import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.vcell.restq.Main; import org.vcell.restq.db.BioModelRestDB; import org.vcell.restq.db.UserRestDB; import org.vcell.restq.models.BioModel; import org.vcell.util.DataAccessException; +import org.vcell.util.ObjectNotFoundException; import org.vcell.util.document.KeyValue; import org.vcell.util.document.User; @@ -28,7 +31,7 @@ public class BioModelResource { private final UserRestDB userRestDB; @Inject - public BioModelResource(BioModelRestDB bioModelRestDB, UserRestDB userRestDB) throws DataAccessException { + public BioModelResource(BioModelRestDB bioModelRestDB, UserRestDB userRestDB) { this.bioModelRestDB = bioModelRestDB; this.userRestDB = userRestDB; } @@ -36,14 +39,22 @@ public BioModelResource(BioModelRestDB bioModelRestDB, UserRestDB userRestDB) th @GET @Path("{bioModelID}") @Operation(operationId = "getBiomodelById", summary = "Get BioModel information in JSON format by ID.") + @APIResponses({ + @APIResponse(responseCode = "200", description = "return BioModel information in JSON format"), + @APIResponse(responseCode = "404", description = "BioModel not found") + }) @Produces(MediaType.APPLICATION_JSON) public BioModel getBioModelInfo(@PathParam("bioModelID") String bioModelID) throws SQLException, DataAccessException, ExpressionException { User vcellUser = userRestDB.getUserFromIdentity(securityIdentity); if (vcellUser == null) { vcellUser = Main.DUMMY_USER; } - BioModelRep bioModelRep = bioModelRestDB.getBioModelRep(new KeyValue(bioModelID), vcellUser); - return BioModel.fromBioModelRep(bioModelRep); + try { + BioModelRep bioModelRep = bioModelRestDB.getBioModelRep(new KeyValue(bioModelID), vcellUser); + return BioModel.fromBioModelRep(bioModelRep); + }catch (ObjectNotFoundException e) { + throw new WebApplicationException("BioModel not found", 404); + } } // TODO: Specify the media type instead of leaving it as wildcard @@ -52,7 +63,7 @@ public BioModel getBioModelInfo(@PathParam("bioModelID") String bioModelID) thro @Operation(operationId = "getBioModelVCML", summary = "Get the BioModel in VCML format.", hidden = true) @Produces(MediaType.APPLICATION_XML) public void getBioModelVCML(@PathParam("bioModelID") String bioModelID){ - + throw new WebApplicationException("Not implemented", 501); } @GET @@ -60,7 +71,7 @@ public void getBioModelVCML(@PathParam("bioModelID") String bioModelID){ @Operation(operationId = "getBioModelSBML", summary = "Get the BioModel in SBML format.", hidden = true) @Produces(MediaType.APPLICATION_XML) public void getBioModelSBML(@PathParam("bioModelID") String bioModelID){ - + throw new WebApplicationException("Not implemented", 501); } @GET @@ -68,7 +79,7 @@ public void getBioModelSBML(@PathParam("bioModelID") String bioModelID){ @Operation(operationId = "getBioModelOMEX", summary = "Get the BioModel in OMEX format.", hidden = true) @Produces(MediaType.MEDIA_TYPE_WILDCARD) public void getBioModelOMEX(@PathParam("bioModelID") String bioModelID){ - + throw new WebApplicationException("Not implemented", 501); } @GET @@ -76,7 +87,7 @@ public void getBioModelOMEX(@PathParam("bioModelID") String bioModelID){ @Operation(operationId = "getBioModelBNGL", summary = "Get the BioModel in BNGL format.", hidden = true) @Produces(MediaType.MEDIA_TYPE_WILDCARD) public void getBioModelBNGL(@PathParam("bioModelID") String bioModelID){ - + throw new WebApplicationException("Not implemented", 501); } @GET @@ -84,7 +95,7 @@ public void getBioModelBNGL(@PathParam("bioModelID") String bioModelID){ @Operation(operationId = "getBioModelDIAGRAM", summary = "Get the BioModels diagram.", hidden = true) @Produces(MediaType.MEDIA_TYPE_WILDCARD) public void getBioModelDiagram(@PathParam("bioModelID") String bioModelID){ - + throw new WebApplicationException("Not implemented", 501); } @POST diff --git a/vcell-rest/src/main/resources/scripts/init.sql b/vcell-rest/src/main/resources/scripts/init.sql index 7ee745f606..7b99bfa4ec 100644 --- a/vcell-rest/src/main/resources/scripts/init.sql +++ b/vcell-rest/src/main/resources/scripts/init.sql @@ -108,3 +108,5 @@ INSERT INTO vc_specialusers VALUES ( 22,'publicationEditors',20,NULL); INSERT INTO vc_specialusers VALUES ( 23,'powerUsers',20,NULL); INSERT INTO vc_specialusers VALUES ( 24,'admins',20,NULL); INSERT INTO vc_publication VALUES ( 25,'Numerical Approach to Spatial Deterministic-Stochastic Models Arising in Cell Biology','Schaff, J.C., Gao, F., Li, Y., Novak, I.L., Slepchenko, B.M.',2016,'Schaff JC, Gao F, Li Y, Novak IL, Slepchenko BM. Numerical Approach to Spatial Deterministic-Stochastic Models Arising in Cell Biology. PLoS Comput Biol. 2016 Dec 13;12(12):e1005236. doi: 10.1371/journal.pcbi.1005236. PMID: 27959915; PMCID: PMC5154471.','27959915','10.1371/journal.pcbi.1005236',NULL,'http://www.vcell.org',NULL,CURRENT_TIMESTAMP ); +INSERT INTO vc_model VALUES ( 26,'ModelName',4,0,NULL,CURRENT_TIMESTAMP,0,NULL,27,NULL,NULL,NULL ); +INSERT INTO vc_biomodel VALUES ( 28,'BioModelName',4,0,NULL,CURRENT_TIMESTAMP,0,NULL,29,26,NULL,NULL ); diff --git a/vcell-rest/src/test/java/org/vcell/restq/TestEndpointUtils.java b/vcell-rest/src/test/java/org/vcell/restq/TestEndpointUtils.java index afc076cd1c..7905f19d21 100644 --- a/vcell-rest/src/test/java/org/vcell/restq/TestEndpointUtils.java +++ b/vcell-rest/src/test/java/org/vcell/restq/TestEndpointUtils.java @@ -1,7 +1,14 @@ package org.vcell.restq; +import cbit.vcell.biomodel.BioModel; +import cbit.vcell.mapping.MathMappingCallbackTaskAdapter; +import cbit.vcell.mapping.SimulationContext; +import cbit.vcell.model.*; import cbit.vcell.modeldb.AdminDBTopLevel; import cbit.vcell.modeldb.DatabaseServerImpl; +import cbit.vcell.parser.Expression; +import cbit.vcell.solver.Simulation; +import cbit.vcell.solver.TimeBounds; import cbit.vcell.xml.XMLSource; import cbit.vcell.xml.XmlHelper; import cbit.vcell.xml.XmlParseException; @@ -17,6 +24,7 @@ import org.vcell.util.document.KeyValue; import org.vcell.util.document.User; +import java.beans.PropertyVetoException; import java.beans.PropertyVetoException; import java.io.IOException; import java.sql.SQLException; @@ -106,4 +114,22 @@ public static Publication defaultPublication(){ publication.setDate(LocalDate.now()); return publication; } + + public static BioModel defaultBiomodel() throws Exception { + BioModel biomodel = new BioModel(null); + Feature comp = biomodel.getModel().createFeature(); + SpeciesContext sc = biomodel.getModel().createSpeciesContext(comp); + SimpleReaction simpleReaction = biomodel.getModel().createSimpleReaction(comp); + simpleReaction.addReactant(sc,1); + simpleReaction.getKinetics().getKineticsParameterFromRole(Kinetics.ROLE_KForward).setExpression(new Expression(1.0)); + SimulationContext simContext1 = biomodel.addNewSimulationContext("application1", SimulationContext.Application.NETWORK_DETERMINISTIC); + SimulationContext.MathMappingCallback callback = new SimulationContext.MathMappingCallback() { + @Override public void setMessage(String message) {} + @Override public void setProgressFraction(float fractionDone) {} + @Override public boolean isInterrupted() { return false;} + }; + Simulation simulation = simContext1.addNewSimulation("simulation1", callback, SimulationContext.NetworkGenerationRequirements.ComputeLongTimeout); + simulation.getSolverTaskDescription().setTimeBounds(new TimeBounds(0, 10)); + return biomodel; + } } diff --git a/vcell-rest/src/test/java/org/vcell/restq/apiclient/PublicationApiTest.java b/vcell-rest/src/test/java/org/vcell/restq/apiclient/PublicationApiTest.java index dff85c2566..b7d92cd54e 100644 --- a/vcell-rest/src/test/java/org/vcell/restq/apiclient/PublicationApiTest.java +++ b/vcell-rest/src/test/java/org/vcell/restq/apiclient/PublicationApiTest.java @@ -1,6 +1,8 @@ package org.vcell.restq.apiclient; +import cbit.vcell.biomodel.BioModel; import cbit.vcell.resource.PropertyLoader; +import cbit.vcell.xml.XmlHelper; import io.quarkus.logging.Log; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.keycloak.client.KeycloakTestClient; @@ -9,19 +11,17 @@ import org.junit.jupiter.api.*; import org.vcell.restclient.ApiClient; import org.vcell.restclient.ApiException; -import org.vcell.restclient.Configuration; +import org.vcell.restclient.api.BioModelResourceApi; import org.vcell.restclient.api.PublicationResourceApi; -import org.vcell.restclient.api.UsersResourceApi; +import org.vcell.restclient.model.BiomodelRef; import org.vcell.restclient.model.Publication; import org.vcell.restq.TestEndpointUtils; import org.vcell.restq.config.CDIVCellConfigProvider; import org.vcell.restq.db.AgroalConnectionFactory; import org.vcell.util.DataAccessException; +import org.vcell.util.document.VersionFlag; import java.sql.SQLException; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; @QuarkusTest @@ -38,11 +38,10 @@ public class PublicationApiTest { private ApiClient aliceAPIClient; private ApiClient bobAPIClient; - private final Publication defaultPub = TestEndpointUtils.defaultPublication(); @BeforeAll - public static void setupConfig(){ + public static void setupConfig() throws Exception { PropertyLoader.setConfigProvider(new CDIVCellConfigProvider()); } @@ -70,7 +69,7 @@ private void addPublications(){ @Test public void guestUserAccess() throws ApiException { PublicationResourceApi publisherAPI = new PublicationResourceApi(aliceAPIClient); - long id = publisherAPI.createPublication(defaultPub); + long id = publisherAPI.createPublication(TestEndpointUtils.defaultPublication()); ApiClient defaultClient = TestEndpointUtils.createUnAuthenticatedAPIClient(testPort); @@ -84,7 +83,7 @@ public void guestUserAccess() throws ApiException { } @Test - public void testAddListRemove() throws ApiException { + public void testAddListRemove() throws Exception { Log.debug("authServerUrl: " + authServerUrl + " to be used later instead of keycloakClient"); PublicationResourceApi apiInstance = new PublicationResourceApi(aliceAPIClient); @@ -93,26 +92,46 @@ public void testAddListRemove() throws ApiException { int initialPubSize = initialPublications.size(); Assertions.assertEquals(1, initialPubSize); + BioModel realBioModel = TestEndpointUtils.defaultBiomodel(); + String bioModelXml = XmlHelper.bioModelToXML(realBioModel, true); + BioModelResourceApi bioModelAPI = new BioModelResourceApi(aliceAPIClient); + String id = bioModelAPI.uploadBioModel(bioModelXml); + org.vcell.restclient.model.BioModel biomodel = bioModelAPI.getBiomodelById(id); + + Publication publication = TestEndpointUtils.defaultPublication(); + BiomodelRef bioModelRef = new BiomodelRef(); + bioModelRef.setBmKey(Long.parseLong(id)); + bioModelRef.setName(biomodel.getName()); + bioModelRef.setOwnerName(biomodel.getOwnerName()); + bioModelRef.setVersionFlag(VersionFlag.Current.getIntValue()); + assert biomodel.getOwnerKey() != null; + bioModelRef.setOwnerKey(Long.parseLong(biomodel.getOwnerKey())); + assert publication.getBiomodelRefs() != null; + publication.getBiomodelRefs().add(bioModelRef); // save publication pub - Long newPubKey = apiInstance.createPublication(defaultPub); + Long newPubKey = apiInstance.createPublication(publication); Assertions.assertNotNull(newPubKey); // test that pubuser can get publication pub Publication pub2 = apiInstance.getPublicationById(newPubKey); + Assertions.assertNotNull(pub2); - // test that there is one publication now and matches pub + // test that there is one more publication now and matches pub List publications = apiInstance.getPublications(); Assertions.assertEquals(initialPubSize + 1, publications.size()); - defaultPub.setPubKey(newPubKey); + publication.setPubKey(newPubKey); Log.error("TODO: fix discrepency with LocalDates (after round trip, not same)"); - defaultPub.setDate(publications.get(initialPubSize + 0).getDate()); - Assertions.assertEquals(defaultPub, publications.get(initialPubSize + 0)); + publication.setDate(publications.get(initialPubSize + 0).getDate()); + Assertions.assertEquals(publication, publications.get(initialPubSize + 0)); // test that pubuser can delete publication pub apiInstance.deletePublication(newPubKey); - // test that there are no publications now + // test that there are no additional publications now List finalPublications = apiInstance.getPublications(); Assertions.assertEquals(initialPubSize, finalPublications.size()); + + // remove added BioModel + bioModelAPI.deleteBioModel(id); } } \ No newline at end of file diff --git a/vcell-server/src/main/java/cbit/vcell/modeldb/BioModelTable.java b/vcell-server/src/main/java/cbit/vcell/modeldb/BioModelTable.java index 9fb763f1ca..4b6c84ff6d 100644 --- a/vcell-server/src/main/java/cbit/vcell/modeldb/BioModelTable.java +++ b/vcell-server/src/main/java/cbit/vcell/modeldb/BioModelTable.java @@ -250,7 +250,7 @@ public String getPreparedStatement_BioModelReps(String conditions, OrderBy order "(" + subquery + " " + additionalConditionsClause + " " + orderByClause + ") " + "where rownum <= ?"; }else if (dbSyntax == DatabaseSyntax.POSTGRES){ - sql = subquery + " LIMIT ?"; + sql = subquery + " " + additionalConditionsClause + " " + orderByClause + " LIMIT ?"; }else throw new RuntimeException("unexpected database syntax "+dbSyntax); }else{ // full query, limit start and limit @@ -261,7 +261,7 @@ public String getPreparedStatement_BioModelReps(String conditions, OrderBy order " where rownum <= ? ) " + "where rnum >= ?"; }else if (dbSyntax == DatabaseSyntax.POSTGRES){ - sql = subquery + " LIMIT ? OFFSET ? "; + sql = subquery + " " + additionalConditionsClause + " " + orderByClause + " LIMIT ? OFFSET ? "; }else throw new RuntimeException("unexpected database syntax "+dbSyntax); } diff --git a/vcell-util/src/main/java/org/vcell/util/BeanUtils.java b/vcell-util/src/main/java/org/vcell/util/BeanUtils.java index ad0a25689c..50eef5dee3 100644 --- a/vcell-util/src/main/java/org/vcell/util/BeanUtils.java +++ b/vcell-util/src/main/java/org/vcell/util/BeanUtils.java @@ -73,38 +73,42 @@ public static Serializable cloneSerializable(Serializable obj) throws ClassNotFo return CompressionUtils.fromSerialized(CompressionUtils.toSerialized(obj)); } - public static int coordinateToIndex(int[] coordinates, int[] bounds){ - // computes linearized index of a position in an n-dimensional matrix - // see also related method indexToCoordinate() - if(coordinates.length != bounds.length) + public static int parameterScanCoordinateToJobIndex(int[] parameterScanCoordinate, int[] scanBounds){ + // Used to look up simulation job index from parameter scans in MathOverrides + // see also jobIndexToScanParameterCoordinate() + // Note 1: scanBounds is the highest zero-based index in each dimension (e.g. 4,5,6 for 3D matrix of size 5x6x7) + // Note 2: PLEASE DO NOT CHANGE MAPPING - it is immortalized by stored simulation job datasets + if(parameterScanCoordinate.length != scanBounds.length) throw new RuntimeException("coordinates and bounds arrays have different lengths"); - int index = 0; - for(int i = 0; i < bounds.length; i++){ - if(coordinates[i] < 0 || bounds[i] < coordinates[i]) throw new RuntimeException("invalid coordinate"); + int jobIndex = 0; + for(int i = 0; i < scanBounds.length; i++){ + if(parameterScanCoordinate[i] < 0 || scanBounds[i] < parameterScanCoordinate[i]) throw new RuntimeException("invalid coordinate"); int offset = 1; - for(int j = i + 1; j < bounds.length; j++){ - offset *= bounds[j] + 1; + for(int j = i + 1; j < scanBounds.length; j++){ + offset *= scanBounds[j] + 1; } - index += coordinates[i] * offset; + jobIndex += parameterScanCoordinate[i] * offset; } - return index; + return jobIndex; } - public static int[] indexToCoordinate(int index, int[] bounds){ - // computes coordinates of a position in an n-dimensional matrix from linearized index - // see also related method coordinateToIndex() - if(index < 0) throw new RuntimeException("invalid index, negative number"); - int[] coordinates = new int[bounds.length]; - for(int i = 0; i < bounds.length; i++){ + public static int[] jobIndexToScanParameterCoordinate(int jobIndex, int[] scanBounds){ + // Used to look up MathOverrides parameter scan indices from simulation Job indices + // see also parameterScanCoordinateToJobIndex() + // Note 1: scanBounds is the highest zero-based index in each dimension (e.g. 4,5,6 for 3D matrix of size 5x6x7) + // Note 2: PLEASE DO NOT CHANGE MAPPING - it is immortalized by stored simulation job datasets + if(jobIndex < 0) throw new RuntimeException("invalid index, negative number"); + int[] parameterScanCoordinates = new int[scanBounds.length]; + for(int i = 0; i < scanBounds.length; i++){ int offset = 1; - for(int j = i + 1; j < bounds.length; j++){ - offset *= bounds[j] + 1; + for(int j = i + 1; j < scanBounds.length; j++){ + offset *= scanBounds[j] + 1; } - coordinates[i] = index / offset; - if(coordinates[i] > bounds[i]) throw new RuntimeException("invalid index, number too large"); - index -= offset * coordinates[i]; + parameterScanCoordinates[i] = jobIndex / offset; + if(parameterScanCoordinates[i] > scanBounds[i]) throw new RuntimeException("invalid index, number too large"); + jobIndex -= offset * parameterScanCoordinates[i]; } - return coordinates; + return parameterScanCoordinates; } public static String forceStringLength(String originalString, int targetLength, String pad, boolean shouldPrependPad){ diff --git a/vcell-util/src/test/java/org/vcell/util/BeanUtilsTest.java b/vcell-util/src/test/java/org/vcell/util/BeanUtilsTest.java new file mode 100644 index 0000000000..0ce65a277d --- /dev/null +++ b/vcell-util/src/test/java/org/vcell/util/BeanUtilsTest.java @@ -0,0 +1,61 @@ +package org.vcell.util; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +@Tag("Fast") +public class BeanUtilsTest { + @Test + public void testExampleMappings() { + int[] scanBounds = {2, 3, 5}; + int[] scanCoordinates = {1, 2, 3}; + int expectedJobIndex = 39; + + // check that index is as expected for scan coordinates + int index = BeanUtils.parameterScanCoordinateToJobIndex(scanCoordinates, scanBounds); + assertEquals(expectedJobIndex, index); + + // Convert index back to coordinates and verify that they match the original coordinates + int[] resultCoordinates = BeanUtils.jobIndexToScanParameterCoordinate(index, scanBounds); + assertArrayEquals(scanCoordinates, resultCoordinates); + } + + @Test + public void testAllScanCoordinatesWithinBounds() { + int[] scanBounds = {4, 5, 6}; + + for (int x = 0; x <= scanBounds[0]; x++) { + for (int y = 0; y <= scanBounds[1]; y++) { + for (int z = 0; z <= scanBounds[2]; z++) { + int[] coordinates = {x, y, z}; + + // Convert coordinates to index + int index = BeanUtils.parameterScanCoordinateToJobIndex(coordinates, scanBounds); + + // Convert index back to coordinates + int[] resultCoordinates = BeanUtils.jobIndexToScanParameterCoordinate(index, scanBounds); + + // Assert that the original coordinates match the result coordinates + assertArrayEquals(coordinates, resultCoordinates); + } + } + } + } + + @Test + public void testAllJobIndicesWithinScanBounds() { + int[] scanBounds = {4, 5, 6}; + int numJobs = (scanBounds[0] + 1) * (scanBounds[1] + 1) * (scanBounds[2] + 1); + + for (int jobIndex = 0; jobIndex < numJobs; jobIndex++) { + // Convert index to coordinates + int[] scanCoordinates = BeanUtils.jobIndexToScanParameterCoordinate(jobIndex, scanBounds); + // Convert coordinates back to index + int resultIndex = BeanUtils.parameterScanCoordinateToJobIndex(scanCoordinates, scanBounds); + + // Assert that the original index matches the result index + assertEquals(jobIndex, resultIndex); + } + } + +} \ No newline at end of file diff --git a/webapp-ng/src/app/app-routing.module.ts b/webapp-ng/src/app/app-routing.module.ts index f16be21302..fce93b2d33 100644 --- a/webapp-ng/src/app/app-routing.module.ts +++ b/webapp-ng/src/app/app-routing.module.ts @@ -7,6 +7,8 @@ import { AuthGuard } from '@auth0/auth0-angular'; import { PublicationListComponent } from './components/publication-list/publication-list.component'; import {LoginSuccessComponent} from "./pages/login-success/login-success.component"; import {AdminComponent} from "./pages/admin/admin.component"; +import {PublicationNewComponent} from "./components/publication-new/publication-new.component"; +import {PublicationDetailComponent} from "./components/publication-detail/publication-detail.component"; const routes: Routes = [ { @@ -24,6 +26,16 @@ const routes: Routes = [ component: PublicationListComponent, canActivate: [AuthGuard], }, + { + path: 'publications/new', + component: PublicationNewComponent, + canActivate: [AuthGuard], + }, + { + path: 'publications/:pubId', + component: PublicationDetailComponent, + canActivate: [AuthGuard], + }, { path: 'error', component: ErrorComponent, diff --git a/webapp-ng/src/app/app.module.ts b/webapp-ng/src/app/app.module.ts index 59960903d7..7b95a78aab 100644 --- a/webapp-ng/src/app/app.module.ts +++ b/webapp-ng/src/app/app.module.ts @@ -56,7 +56,6 @@ export function apiConfigFactory(baseuriConfigService: BaseuriConfigService) { LoadingComponent, LoginSuccessComponent, PublicationListComponent, - PublicationEditComponent, VcellIdentityComponent, ErrorComponent ], @@ -85,6 +84,7 @@ export function apiConfigFactory(baseuriConfigService: BaseuriConfigService) { ApiModule, MatCardModule, MatCheckboxModule, + PublicationEditComponent, ], providers: [ { @@ -120,5 +120,8 @@ export function apiConfigFactory(baseuriConfigService: BaseuriConfigService) { ], bootstrap: [AppComponent], + exports: [ + PublicationEditComponent + ] }) export class AppModule {} diff --git a/webapp-ng/src/app/components/publication-confirm-dialog/publication-confirm-dialog.component.ts b/webapp-ng/src/app/components/publication-confirm-dialog/publication-confirm-dialog.component.ts new file mode 100644 index 0000000000..cd4710acc6 --- /dev/null +++ b/webapp-ng/src/app/components/publication-confirm-dialog/publication-confirm-dialog.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import {MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle} from '@angular/material/dialog'; +import {MatButtonModule} from "@angular/material/button"; + +@Component({ + selector: 'app-confirm-dialog', + template: ` +

Confirm Deletion

+
Are you sure you want to delete this publication?
+
+ + +
+ `, + imports: [ + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatButtonModule + ], + standalone: true +}) +export class PublicationConfirmDialogComponent { + constructor(public dialogRef: MatDialogRef) {} + + onCancel(): void { + this.dialogRef.close(false); + } + + onConfirm(): void { + this.dialogRef.close(true); + } +} diff --git a/webapp-ng/src/app/components/publication-detail/publication-detail.component.css b/webapp-ng/src/app/components/publication-detail/publication-detail.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webapp-ng/src/app/components/publication-detail/publication-detail.component.html b/webapp-ng/src/app/components/publication-detail/publication-detail.component.html new file mode 100644 index 0000000000..5141cf7f17 --- /dev/null +++ b/webapp-ng/src/app/components/publication-detail/publication-detail.component.html @@ -0,0 +1,8 @@ +
+

{{ "Publication id=" + publication.pubKey }}

+ +

Loading publication...

+

+ + + diff --git a/webapp-ng/src/app/components/publication-detail/publication-detail.component.spec.ts b/webapp-ng/src/app/components/publication-detail/publication-detail.component.spec.ts new file mode 100644 index 0000000000..a9ec2e3416 --- /dev/null +++ b/webapp-ng/src/app/components/publication-detail/publication-detail.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PublicationDetailComponent } from './publication-detail.component'; + +describe('PublicationDetailComponent', () => { + let component: PublicationDetailComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PublicationDetailComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PublicationDetailComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/webapp-ng/src/app/components/publication-detail/publication-detail.component.ts b/webapp-ng/src/app/components/publication-detail/publication-detail.component.ts new file mode 100644 index 0000000000..886d20ef84 --- /dev/null +++ b/webapp-ng/src/app/components/publication-detail/publication-detail.component.ts @@ -0,0 +1,87 @@ +import {Component, Input, OnInit} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import {Publication} from "../../core/modules/openapi"; +import {PublicationService} from "../publication-list/publication.service"; +import {PublicationEditComponent} from "../publication-edit/publication-edit.component"; +import {ActivatedRoute, Router} from "@angular/router"; +import {isInteger} from "@ng-bootstrap/ng-bootstrap/util/util"; +import {MatButtonModule} from "@angular/material/button"; +import {MatDialog} from "@angular/material/dialog"; +import {PublicationConfirmDialogComponent} from "../publication-confirm-dialog/publication-confirm-dialog.component"; + +@Component({ + selector: 'app-publication-detail', + standalone: true, + imports: [CommonModule, PublicationEditComponent, MatButtonModule], + templateUrl: './publication-detail.component.html', + styleUrl: './publication-detail.component.css' +}) +export class PublicationDetailComponent implements OnInit { + @Input() publication!: Publication; + + constructor( + private publicationService: PublicationService, + private router: Router, + private route: ActivatedRoute, + private dialog: MatDialog + ) {} + + saveUpdatedPublication(pub: Publication) { + this.publicationService.updatePublication(pub).subscribe({ + next: () => { + console.log("Publication updated"); + this.router.navigate(['/publications']); + }, + error: (err) => { + console.error("Error updating publication", err); + } + }); + } + + deletePublication(pub: Publication | undefined) { + if (pub == undefined || pub.pubKey == undefined) { + console.error("publication or publication key is undefined") + return; + } + + const dialogRef = this.dialog.open(PublicationConfirmDialogComponent); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + this.deletePublicationConfirmed(pub); + } + }); + } + + deletePublicationConfirmed(pub: Publication) { + this.publicationService.deletePublication(pub.pubKey!).subscribe({ + next: () => { + console.log("Publication deleted"); + this.router.navigate(['/publications']); + }, + error: (err) => { + console.error("Error deleting publication", err); + } + }); + } + + cancel() { + console.log("Update cancelled"); + this.router.navigate(['/publications']); + } + + ngOnInit(): void { + const pubId = this.route.snapshot.paramMap.get('pubId'); + if (pubId && Number.isInteger(parseInt(pubId))) { + this.publicationService.getPublicationById(parseInt(pubId)).subscribe({ + next: (publication) => { + this.publication = publication; + }, + error: (err) => { + console.error('Error fetching publication', err); + } + }); + } + } + +} diff --git a/webapp-ng/src/app/components/publication-edit/publication-edit.component.css b/webapp-ng/src/app/components/publication-edit/publication-edit.component.css index e860fd9395..cfc6610f8d 100644 --- a/webapp-ng/src/app/components/publication-edit/publication-edit.component.css +++ b/webapp-ng/src/app/components/publication-edit/publication-edit.component.css @@ -1,6 +1,34 @@ .publication-edit-container { - /* Example style */ - padding: 20px; - border: 1px solid #ccc; - border-radius: 5px; - } \ No newline at end of file + display: grid; + grid-template-columns: 1fr; + gap: 1rem; +} + +@media (min-width: 600px) { + .publication-edit-container { + grid-template-columns: auto 1fr; + } +} + +.full-width { + width: 100%; +} + +.button-row { + display: flex; + justify-content: flex-end; + gap: 1rem; +} + +.sub-form { + margin-left: 40px; + margin-right: 40px; +} + +.list-item { + margin-top: 0; + margin-bottom: 0; + /*height: min-content; !* Adjust this value as needed *!*/ + /*display: flex;*/ + align-items: center; +} diff --git a/webapp-ng/src/app/components/publication-edit/publication-edit.component.html b/webapp-ng/src/app/components/publication-edit/publication-edit.component.html index 59f031eae4..8d541f8176 100644 --- a/webapp-ng/src/app/components/publication-edit/publication-edit.component.html +++ b/webapp-ng/src/app/components/publication-edit/publication-edit.component.html @@ -1,64 +1,95 @@
-
-

{{ "Edit Publication, key=" + publication.pubKey }}

-

New Publication

+
+ + Title + + Title is required + + + + Authors + + Authors are required + + + + Year + + Year is required + + + + Citation + + Citation is required + + + + Pubmed ID + + + + + DOI + + + + + URL + + + +
+

Biomodel IDs

+
+ + + {{ "BioModel (" + biomodelRef.ownerName + " : " + biomodelRef.bmKey + " : " + biomodelRef.name + ")" }} + + + +
+ + Enter Biomodel Key + + +
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - -
-
+ +
+ +
+

Mathmodel IDs

+
+ + + {{ "MathModel (" + mathmodelRef.ownerName + " : " + mathmodelRef.mmKey + " : " + mathmodelRef.name + ")" }} + + + +
+ + Enter Mathmodel Key + + + +
+ + + + + + +
+ + +
+ +
diff --git a/webapp-ng/src/app/components/publication-edit/publication-edit.component.ts b/webapp-ng/src/app/components/publication-edit/publication-edit.component.ts index 6fa38e4843..5e6d08ca68 100644 --- a/webapp-ng/src/app/components/publication-edit/publication-edit.component.ts +++ b/webapp-ng/src/app/components/publication-edit/publication-edit.component.ts @@ -1,15 +1,92 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core'; -import { Publication } from 'src/app/core/modules/openapi/model/publication'; // Adjust the import path as necessary +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {Publication} from 'src/app/core/modules/openapi/model/publication'; +import {FormsModule} from "@angular/forms"; +import {CommonModule} from "@angular/common"; +import {MatInputModule} from "@angular/material/input"; +import {MatButtonModule} from "@angular/material/button"; +import {BioModel, BiomodelRef, BioModelResourceService} from "../../core/modules/openapi"; +import {HttpResponse} from "@angular/common/http"; +import {MatCardModule} from "@angular/material/card"; +import {MatIconModule} from "@angular/material/icon"; +import {MatSnackBar} from "@angular/material/snack-bar"; // Adjust the import path as necessary @Component({ selector: 'app-publication-edit', templateUrl: './publication-edit.component.html', + standalone: true, + imports: [CommonModule, FormsModule, MatInputModule, MatButtonModule, MatCardModule, MatIconModule], styleUrls: ['./publication-edit.component.css'] }) export class PublicationEditComponent { @Input() publication!: Publication;// The publication to edit @Output() save = new EventEmitter(); // Event to emit when the publication is saved @Output() cancel = new EventEmitter(); // Event to emit when the edit is canceled + newBiomodelKey: number | undefined; + newMathmodelKey: number | undefined; + + constructor(private bioModelService: BioModelResourceService, private snackBar: MatSnackBar) { } + + addBiomodelRef(key: number | undefined) { + console.log("addBiomodelRef, key: " + key); + if (!key) { + return; + } + this.bioModelService.getBiomodelById(key.toString(), "response").subscribe({ + next: (biomodelResponse: HttpResponse) => { + if (biomodelResponse.status !== 200) { + this.snackBar.open("Error fetching biomodel", "Dismiss", {duration: 5000}); + console.error("Error fetching biomodel", biomodelResponse); + return; + } + if (!this.publication.biomodelRefs) { + this.publication.biomodelRefs = []; + } + const biomodel = biomodelResponse.body as BioModel; + const biomodelRef : BiomodelRef = { + bmKey: biomodel.bmKey!=null ? Number.parseInt(biomodel.bmKey) : -1, + name: biomodel.name, + ownerName: biomodel.ownerName, + ownerKey: biomodel.ownerKey!=null ? Number.parseInt(biomodel.ownerKey) : -1, + versionFlag: biomodel.privacy + }; + this.publication.biomodelRefs.push(biomodelRef); + this.newBiomodelKey = undefined; + }, + error: (err: any) => { + this.snackBar.open("Error fetching biomodel", "Dismiss", {duration: 5000}); + console.error("Error fetching biomodel", err); + } + }); + } + + removeBiomodelRef(index: number) { + console.log("removeBiomodelRef, index: " + index); + if (!this.publication.biomodelRefs) { + return; + } + this.publication.biomodelRefs.splice(index, 1); + } + + addMathmodelRef(key: number | undefined) { + if (!key) { + return; + } + if (!this.publication.mathmodelRefs) { + this.publication.mathmodelRefs = []; + } + this.snackBar.open("adding MathModels not yet implemented - requires API changes", "Dismiss", {duration: 5000}); + console.error("add MathmodelRef not yet implemented"); + // this.publicationService.getMathmodelRef(key).subscribe((mathmodelRef: MathmodelRef) => { + // this.publication.mathmodelRefs.push(mathmodelRef); + // }); + } + + removeMathmodelRef(index: number) { + if (!this.publication.mathmodelRefs) { + return; + } + this.publication.mathmodelRefs.splice(index, 1); + } onSave() { this.save.emit(this.publication); @@ -18,4 +95,5 @@ export class PublicationEditComponent { onCancel() { this.cancel.emit(); } + } diff --git a/webapp-ng/src/app/components/publication-list/publication-list.component.html b/webapp-ng/src/app/components/publication-list/publication-list.component.html index f63250c134..35d234c8a2 100644 --- a/webapp-ng/src/app/components/publication-list/publication-list.component.html +++ b/webapp-ng/src/app/components/publication-list/publication-list.component.html @@ -1,49 +1,61 @@

Publications

- + +
+ +
+
+ + Filter + + - PubKey - {{ row.pubKey }} + PubKey + + {{ row.pubKey }} + - Title - {{ row.title }} + Title + {{ row.title }} - Authors - {{ row.authors }} + Authors + {{ row.authors }} - Year - {{ row.year }} + Year + {{ row.year }} - Citation - {{ row.citation }} + Citation + {{ row.citation }} - Pubmed ID - {{ row.pubmedid }} + PMID + {{ row.pubmedid }} - DOI - {{ row.doi }} - - - Endnote ID - {{ row.endnoteid }} + DOI + {{ row.doi }} + + + + - URL - {{ row.url }} - - - Witt ID - {{ row.wittid }} + URL + {{ row.url }} + + + + - Biomodels - + Biomodels + {{ ref.bmKey }} @@ -51,8 +63,8 @@

Publications

- Mathmodels - + Mathmodels + {{ ref.mmKey }} @@ -60,40 +72,12 @@

Publications

- Date - {{ row.date }} + Date + {{ row.date }} - - Edit - - - - - - Delete - - - - - - - - - - - - - - + +
- - - -
- - -
diff --git a/webapp-ng/src/app/components/publication-list/publication-list.component.less b/webapp-ng/src/app/components/publication-list/publication-list.component.less index 7a3828256c..d6ca56f859 100644 --- a/webapp-ng/src/app/components/publication-list/publication-list.component.less +++ b/webapp-ng/src/app/components/publication-list/publication-list.component.less @@ -14,3 +14,64 @@ body, html { white-space: normal !important; word-wrap: break-word !important; } + +.new-button-container { + display: flex; + justify-content: flex-end; + margin-bottom: 10px; +} + +.column-pubKey { + width: 10%; + max-width: 100px; +} + +.column-title { + width: 20%; + max-width: 200px; +} + +.column-authors { + width: 15%; + max-width: 150px; +} + +.column-year { + width: 10%; + max-width: 50px; +} + +.column-citation { + width: 20%; + max-width: 200px; +} + +.column-pubmedid { + width: 10%; + max-width: 100px; +} + +.column-doi { + width: 10%; + max-width: 100px; +} + +.column-url { + width: 15%; + max-width: 150px; +} + +.column-biomodelRefs { + width: 10%; + max-width: 100px; +} + +.column-mathmodelRefs { + width: 10%; + max-width: 100px; +} + +.column-date { + width: 10%; + max-width: 100px; +} diff --git a/webapp-ng/src/app/components/publication-list/publication-list.component.ts b/webapp-ng/src/app/components/publication-list/publication-list.component.ts index b5382fe27e..2e8c659429 100644 --- a/webapp-ng/src/app/components/publication-list/publication-list.component.ts +++ b/webapp-ng/src/app/components/publication-list/publication-list.component.ts @@ -4,7 +4,7 @@ import {PublicationService} from './publication.service'; import {MatTableDataSource} from '@angular/material/table'; import {MatSort} from '@angular/material/sort'; import {MatPaginator} from '@angular/material/paginator'; -import {async, Observable} from 'rxjs'; +import {Observable} from 'rxjs'; import {AuthorizationService} from 'src/app/services/authorization.service'; @@ -17,9 +17,8 @@ export class PublicationListComponent implements OnInit { publications = new MatTableDataSource(); // displayedColumns = [ "pubKey", "title", "authors", "year", "citation", "pubmedid", "doi", "endnoteid", "url", // "wittid", "biomodelRefs", "mathmodelRefs", "date"]; - displayedColumns = [ "title", "authors", "year", "citation", "pubmedid", + displayedColumns = [ "pubKey", "title", "authors", "year", "citation", "pubmedid", "biomodelRefs", "mathmodelRefs", "date"]; - editingPublication: Publication | null = null; isCurator$: Observable; @ViewChild(MatSort) sort!: MatSort; @@ -39,71 +38,9 @@ export class PublicationListComponent implements OnInit { this.publicationService.refresh(); } - onNew() { - const pub: Publication = { - title: "title", - authors: [ - "string" - ], - year: 2023, - citation: "citation", - pubmedid: "pubmed-id", - doi: "doi", - endnoteid: 0, - url: "url", - wittid: 0, - biomodelRefs: [ - - ], - mathmodelRefs: [ - - ], - date: "2023-12-28" - }; - this.startEdit(pub); - } - - onEdit(pub: Publication) { - // nothing to do - console.log("editing publication "+pub.title); - this.startEdit(pub) - } - - onDelete(pub: Publication) { - // nothing to do - this.publicationService.deletePublication(pub.pubKey!).subscribe((pub) => { - console.log(pub); - }); - - } - - applyFilter(filterText: string) { - this.publications.filter = filterText.toLowerCase(); - } - applyFilterTarget(eventTarget: EventTarget | null) { - // //get publication object from this row - // const pub: Publication = eventTarget.; const filterValue = (eventTarget as HTMLInputElement).value; this.publications.filter = filterValue.toLowerCase(); } -startEdit(pub: Publication) { - const clonedPub = { ...pub }; - this.editingPublication = clonedPub; -} - -saveEdit(pub: Publication) { - // Call the service to save the publication - this.publicationService.updatePublication(pub).subscribe(() => { - // Handle successful save - this.editingPublication = null; - }); -} - -cancelEdit() { - this.editingPublication = null; -} - - protected readonly async = async; } diff --git a/webapp-ng/src/app/components/publication-new/publication-new.component.css b/webapp-ng/src/app/components/publication-new/publication-new.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webapp-ng/src/app/components/publication-new/publication-new.component.html b/webapp-ng/src/app/components/publication-new/publication-new.component.html new file mode 100644 index 0000000000..e8973d894f --- /dev/null +++ b/webapp-ng/src/app/components/publication-new/publication-new.component.html @@ -0,0 +1,2 @@ +

publication-new works!

+ diff --git a/webapp-ng/src/app/components/publication-new/publication-new.component.spec.ts b/webapp-ng/src/app/components/publication-new/publication-new.component.spec.ts new file mode 100644 index 0000000000..4c8aec3587 --- /dev/null +++ b/webapp-ng/src/app/components/publication-new/publication-new.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PublicationNewComponent } from './publication-new.component'; + +describe('PublicationNewComponent', () => { + let component: PublicationNewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PublicationNewComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PublicationNewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/webapp-ng/src/app/components/publication-new/publication-new.component.ts b/webapp-ng/src/app/components/publication-new/publication-new.component.ts new file mode 100644 index 0000000000..655bce738e --- /dev/null +++ b/webapp-ng/src/app/components/publication-new/publication-new.component.ts @@ -0,0 +1,59 @@ +import {Component} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Publication} from "../../core/modules/openapi"; +import {PublicationService} from "../publication-list/publication.service"; +import {PublicationEditComponent} from "../publication-edit/publication-edit.component"; +import {Router} from "@angular/router"; + +@Component({ + selector: 'app-publication-new', + standalone: true, + imports: [CommonModule, PublicationEditComponent], + templateUrl: './publication-new.component.html', + styleUrl: './publication-new.component.css' +}) +export class PublicationNewComponent { + newPublication: Publication; + + constructor(private publicationService: PublicationService, private router: Router) { + this.newPublication = this.onNew(); + } + + onNew(): Publication { + return { + title: "title", + authors: [ + "string" + ], + year: 2023, + citation: "citation", + pubmedid: "pubmed-id", + doi: "doi", + endnoteid: 0, + url: "url", + wittid: 0, + biomodelRefs: [], + mathmodelRefs: [], + date: "2023-12-28" + }; + } + + saveNewPublication(pub: Publication) { + // Call the service to save the publication + this.publicationService.createPublication(pub).subscribe({ + next: () => { + console.log("saved publication"); + this.router.navigate(['/publications']); + }, + error: (err) => { + console.error("Error saving publication", err); + } + }); + } + + cancel() { + console.log("Creation cancelled"); + this.router.navigate(['/publications']); + } + +}