diff --git a/.github/workflows/test-cdbportal.yml b/.github/workflows/test-cdbportal.yml new file mode 100644 index 000000000..4e341ba0f --- /dev/null +++ b/.github/workflows/test-cdbportal.yml @@ -0,0 +1,44 @@ +name: Test CDB Portal + +on: + pull_request: + paths: + - 'src/java/CdbWebPortal/src/java/**' + - 'tools/developer_tools/code_testing/CdbWebPortalTest/**' + - '.github/workflows/test-cdbportal.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + name: Run CDB Portal Java Tests + runs-on: ubuntu-latest + + defaults: + run: + working-directory: tools/developer_tools/code_testing/CdbWebPortalTest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 11 and Maven cache + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '11' + cache: maven + cache-dependency-path: tools/developer_tools/code_testing/CdbWebPortalTest/pom.xml + + - name: Run tests + run: mvn --batch-mode --no-transfer-progress test + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: cdb-portal-test-results + path: tools/developer_tools/code_testing/CdbWebPortalTest/target/surefire-reports/ + if-no-files-found: warn diff --git a/.gitignore b/.gitignore index 1c99e42d2..a35762907 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ docs/python/_build/ # Temporary testing csv files tools/developer_tools/python-client/packages/cli/cdbCli/service/cli/Spreadsheets/ +/tools/developer_tools/portal_testing/PythonSeleniumTest/conda-env diff --git a/CLAUDE.md b/CLAUDE.md index 123492a39..49430ada4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,8 +63,8 @@ mvn test -Dtest=ClassName#methodName # single test # Python API tests — require the portal to be running on localhost:8080 cd tools/developer_tools/python-client && ./generatePyClient.sh http://localhost:8080/cdb -cd tools/developer_tools/python-client/test && pytest api_test.py -pytest api_test.py::TestClass::test_method # single test +cd tools/developer_tools/python-client && pytest test/ +pytest test/item_test.py::ItemTest::test_method # single test # Selenium GUI tests cd tools/developer_tools/portal_testing/PythonSeleniumTest && pytest gui_test.py @@ -127,7 +127,7 @@ MariaDB 10.5. Authoritative schema is the set of `create_cdb_tables.sql`, `creat - **`cdb` vs `cdb_dev`**: every db, deploy, undeploy, and backup target has a `-dev` variant that targets a separate database name and Payara domain, so a developer can run both side-by-side. When adding new admin scripts, follow the same pattern (first arg is DB name, defaulting to `cdb`). - **Single canonical version string**: run `make prepare-release` (`sbin/cdb_prepare_release.py`) to bump `etc/version`, `openapi.yaml`, the web `?v=` cache-busters, the python-client `packages/*/pyproject.toml`/conda recipes, and scaffold `docs/release-notes/.md` together. It also flags any file that drifted out of sync in a prior manual release, and refreshes `uv.lock`. - **Plugins** under `tools/developer_tools/cdb_plugins/plugins//` extend both the Java portal (deployed via `make deploy-cdb-plugin` → `update_plugin_generated_files.py`) and the Python web service. `pluginTemplates/` is the scaffold. -- **REST changes touch three places**: the Java route, `openapi.yaml`, and (after `generatePyClient.sh`) the regenerated `cdbApi/` package. The Python API tests in `tools/developer_tools/python-client/test/api_test.py` are the integration check. +- **REST changes touch three places**: the Java route, `openapi.yaml`, and (after `generatePyClient.sh`) the regenerated `cdbApi/` package. The Python API tests under `tools/developer_tools/python-client/test/` are the integration check. ## Reference docs diff --git a/README.md b/README.md index a0c1c99fe..ea33bfed3 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,15 @@ The python web service is deprecated but is still a part of the distribution for ./sbin/cdbWebService.sh ``` +## Running Tests + +```sh +source setup.sh +make test +``` + +See the test setup in `tools/developer_tools/{python-client,portal_testing/PythonSeleniumTest}/README.md`. + ## Preparing a Release `make prepare-release` bumps the version string across the repo (`etc/version`, `openapi.yaml`, web asset cache-busters, python-client `packages/*/pyproject.toml`/conda recipes) and scaffolds `docs/release-notes/.md`. Run it, review the diff, then commit. ```sh diff --git a/db/sql/test/populate_entity_info.sql b/db/sql/test/populate_entity_info.sql index 00d3bc47b..0de8ce640 100644 --- a/db/sql/test/populate_entity_info.sql +++ b/db/sql/test/populate_entity_info.sql @@ -185,6 +185,9 @@ INSERT INTO `entity_info` VALUES (183,1,1,1,'2025-03-05 08:52:22',1,'2025-03-05 09:00:46',1,NULL,NULL), (184,1,1,1,'2025-04-02 08:43:08',1,'2025-04-02 08:44:46',1,NULL,NULL), (185,1,1,1,'2025-04-02 08:43:08',1,'2025-04-02 08:43:08',1,NULL,NULL), -(186,1,1,1,'2025-04-02 08:43:31',1,'2025-04-02 08:43:31',1,NULL,NULL); +(186,1,1,1,'2025-04-02 08:43:31',1,'2025-04-02 08:43:31',1,NULL,NULL), +(187,1,1,1,'2026-10-27 10:43:43',1,'2026-10-27 10:44:07',1,NULL,NULL), +(188,1,1,1,'2026-10-27 10:44:00',1,'2026-10-27 10:44:00',1,NULL,NULL), +(189,1,1,1,'2026-10-27 10:44:00',1,'2026-10-27 10:44:19',1,NULL,NULL); /*!40000 ALTER TABLE `entity_info` ENABLE KEYS */; UNLOCK TABLES; diff --git a/db/sql/test/populate_item.sql b/db/sql/test/populate_item.sql index 959c0b77d..bdd687ec2 100644 --- a/db/sql/test/populate_item.sql +++ b/db/sql/test/populate_item.sql @@ -126,6 +126,8 @@ INSERT INTO `item` VALUES (124,2,'Item With Connectors 2',NULL,'','',NULL), (125,9,'TEST CABLE 2',NULL,'KABEL',NULL,NULL), (126,6,'Item with Ports 3',NULL,NULL,NULL,NULL), -(127,2,'Item With Connectors 3',NULL,'','',NULL); +(127,2,'Item With Connectors 3',NULL,'','',NULL), +(128,6,'Another Top Level ',NULL,NULL,NULL,NULL), +(129,6,'Inventory Assignment Test Destination',NULL,NULL,NULL,NULL); /*!40000 ALTER TABLE `item` ENABLE KEYS */; UNLOCK TABLES; diff --git a/db/sql/test/populate_item_element.sql b/db/sql/test/populate_item_element.sql index 66d083f28..1cf2d91c5 100644 --- a/db/sql/test/populate_item_element.sql +++ b/db/sql/test/populate_item_element.sql @@ -185,6 +185,9 @@ INSERT INTO `item_element` VALUES (183,NULL,125,NULL,71,NULL,NULL,0,1,'Cable with metadata and endpoints with ports defined',NULL,183), (184,'E3',120,126,NULL,NULL,NULL,0,1,NULL,2.00,185), (185,NULL,126,NULL,127,NULL,NULL,0,1,'',NULL,184), -(186,NULL,127,NULL,NULL,NULL,NULL,0,1,'',NULL,186); +(186,NULL,127,NULL,NULL,NULL,NULL,0,1,'',NULL,186), +(187,NULL,128,NULL,NULL,NULL,NULL,0,1,'',NULL,187), +(188,'E1',128,129,NULL,NULL,NULL,0,1,NULL,0.00,188), +(189,NULL,129,NULL,40,NULL,NULL,0,1,'',NULL,189); /*!40000 ALTER TABLE `item_element` ENABLE KEYS */; UNLOCK TABLES; diff --git a/db/sql/test/populate_item_element_history.sql b/db/sql/test/populate_item_element_history.sql index f65eff9f8..f274d7e9f 100644 --- a/db/sql/test/populate_item_element_history.sql +++ b/db/sql/test/populate_item_element_history.sql @@ -203,6 +203,10 @@ INSERT INTO `item_element_history` VALUES (200,'E3',184,'Machine2',120,'Item with Ports 3',126,NULL,NULL,NULL,NULL,0,1,NULL,2.00,'2025-04-02 08:43:21',1), (201,NULL,185,'Item with Ports 3',NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,1,'',NULL,'2025-04-02 08:43:21',1), (202,NULL,186,'Item With Connectors 3',NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,1,'',NULL,'2025-04-02 08:43:31',1), -(203,NULL,185,'Item with Ports 3',NULL,NULL,NULL,'Item With Connectors 3',127,NULL,NULL,0,1,'',NULL,'2025-04-02 08:44:46',1); +(203,NULL,185,'Item with Ports 3',NULL,NULL,NULL,'Item With Connectors 3',127,NULL,NULL,0,1,'',NULL,'2025-04-02 08:44:46',1), +(204,NULL,187,'Another Top Level ',NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,1,'',NULL,'2026-10-27 10:43:43',1), +(205,'E1',188,'Another Top Level ',128,'Inventory Assignment Test Destination',129,NULL,NULL,NULL,NULL,0,1,NULL,0.00,'2026-10-27 10:44:07',1), +(206,NULL,189,'Inventory Assignment Test Destination',NULL,NULL,NULL,NULL,NULL,NULL,NULL,0,1,'',NULL,'2026-10-27 10:44:07',1), +(207,NULL,189,'Inventory Assignment Test Destination',NULL,NULL,NULL,'Test Item',40,NULL,NULL,0,1,'',NULL,'2026-10-27 10:44:19',1); /*!40000 ALTER TABLE `item_element_history` ENABLE KEYS */; UNLOCK TABLES; diff --git a/db/sql/test/populate_item_item_project.sql b/db/sql/test/populate_item_item_project.sql index 2da8b303e..d4e081c2f 100644 --- a/db/sql/test/populate_item_item_project.sql +++ b/db/sql/test/populate_item_item_project.sql @@ -118,6 +118,8 @@ INSERT INTO `item_item_project` VALUES (124,3), (125,3), (126,3), -(127,3); +(127,3), +(128,3), +(129,3); /*!40000 ALTER TABLE `item_item_project` ENABLE KEYS */; UNLOCK TABLES; diff --git a/db/sql/test/populate_log.sql b/db/sql/test/populate_log.sql index 5e5d5110c..bac533291 100644 --- a/db/sql/test/populate_log.sql +++ b/db/sql/test/populate_log.sql @@ -8,6 +8,10 @@ INSERT INTO `log` VALUES (5,'User: cdb | Created: Template Holder [Item Id: 116]','2023-10-09 10:24:50',1,NULL,NULL,NULL), (6,'User: cdb | Updated: Template Holder [Item Id: 116]','2023-10-09 10:25:54',1,NULL,NULL,NULL), (7,'Authentication Succeeded: cdb','2023-10-12 13:08:58',1,NULL,NULL,NULL), -(8,'User: cdb | Created: TEST MAARC [Item Id: 119]','2023-10-12 13:09:09',1,NULL,NULL,NULL); +(8,'User: cdb | Created: TEST MAARC [Item Id: 119]','2023-10-12 13:09:09',1,NULL,NULL,NULL), +(9,'Authentication Succeeded: cdb','2026-10-27 10:43:40',1,NULL,NULL,NULL), +(10,'User: cdb | Created: Another Top Level [Item Id: 128]','2026-10-27 10:43:53',1,NULL,NULL,NULL), +(11,'User: cdb | Updated: Another Top Level [Item Id: 128]','2026-10-27 10:44:07',1,NULL,NULL,NULL), +(12,'User: cdb | Updated: Inventory Assignment Test Destination [Item Id: 129]','2026-10-27 10:44:19',1,NULL,NULL,NULL); /*!40000 ALTER TABLE `log` ENABLE KEYS */; UNLOCK TABLES; diff --git a/db/sql/test/populate_system_log.sql b/db/sql/test/populate_system_log.sql index 2c4905ff9..afa4ef96c 100644 --- a/db/sql/test/populate_system_log.sql +++ b/db/sql/test/populate_system_log.sql @@ -8,6 +8,10 @@ INSERT INTO `system_log` VALUES (5,3), (6,3), (7,2), -(8,3); +(8,3), +(9,2), +(10,3), +(11,3), +(12,3); /*!40000 ALTER TABLE `system_log` ENABLE KEYS */; UNLOCK TABLES; diff --git a/sbin/cdb_test.sh b/sbin/cdb_test.sh index ba00a2478..b64aad8b0 100755 --- a/sbin/cdb_test.sh +++ b/sbin/cdb_test.sh @@ -22,15 +22,27 @@ cd $MY_DIR/.. CDB_DIST_DIR=`pwd` cd $CURRENT_DIR -$MY_DIR/cdb_backup_db.sh - -timestamp=`date +%Y%m%d` -CDB_BACKUP_DIR=$CDB_INSTALL_DIR/backup/cdb/$timestamp +printf "\nBack up the current database and restore it after testing? [Y/n] " +read -r backup_response + +backup_database=1 +case "$backup_response" in + [Nn]*) backup_database=0 ;; + *) + $MY_DIR/cdb_backup_db.sh + timestamp=`date +%Y%m%d` + CDB_BACKUP_DIR=$CDB_INSTALL_DIR/backup/cdb/$timestamp + ;; +esac yes $db_password | $MY_DIR/cdb_create_db.sh cdb $CDB_DIST_DIR/db/sql/test || exit 1 +printf "\nRestart Payara so it uses the newly deployed test database, then press enter to continue..." +read -r + # Regenerate the API with current code base -API_CLIENT_PATH=$CDB_DIST_DIR/tools/developer_tools/python-client/ +API_CLIENT_PATH=$CDB_DIST_DIR/tools/developer_tools/python-client +export PYTHONPATH="$API_CLIENT_PATH/packages/api:$API_CLIENT_PATH/packages/cli${PYTHONPATH:+:$PYTHONPATH}" cd $API_CLIENT_PATH ./generatePyClient.sh http://localhost:8080/cdb @@ -42,59 +54,24 @@ cd $CDB_DIST_DIR/tools/developer_tools/code_testing/CdbWebPortalTest mvn test printf "$PRINTF_HEADER" "API" -cd $API_CLIENT_PATH/test -pytest api_test.py +cd $API_CLIENT_PATH +pytest test/ printf "$PRINTF_HEADER" "Selenium" cd $CDB_DIST_DIR/tools/developer_tools/portal_testing/PythonSeleniumTest -if [ ! -d "support_bin" ]; then - mkdir support_bin -fi - -cd support_bin - -skiptest=0 -needs_download=1 - -chromedriver_version_start=`google-chrome --version | grep -oP "[^A-Z ][0-9]+.[0-9]+.[0-9]+"` -chromedriver_version_regex="$chromedriver_version_start.[0-9]+/chromedriver_linux64.zip" -chromedriver_version=(`curl https://chromedriver.storage.googleapis.com/ | grep -oP $chromedriver_version_regex`) -echo ${chromedriver_version[0]} - -count_results=`echo $chromedriver_version | wc -l` - -if [ $count_results > 1 ]; then - chromedriver_version=`echo ${chromedriver_version%%$'\n'*}` -fi - -if [ -f "chromedriver" ]; then - current_version_result=`./chromedriver --version | grep -oP $chromedriver_version` - if [[ -z "$current_version_result" ]]; then - rm chromedriver - else - needs_download=0 - fi -fi - -if [[ $needs_download == 1 ]]; then - wget https://chromedriver.storage.googleapis.com/$chromedriver_version || skiptest=1 - unzip chromedriver_linux64.zip - rm chromedriver_linux64.zip -fi +# Selenium Manager resolves and downloads a compatible driver when Chrome starts. +pytest tests/ -n auto --dist loadfile --reruns 1 --reruns-delay 1 -rR - -if [[ $skiptest == 0 ]]; then - export PATH=`pwd`:$PATH - cd .. - pytest gui_test.py +if [[ $backup_database == 1 ]]; then + printf "\n\nRestore the original database from the backup? [Y/n] " + read -r restore_response + case "$restore_response" in + [Nn]*) printf "The test database remains installed as cdb.\n" ;; + *) yes $db_password | $MY_DIR/cdb_create_db.sh cdb $CDB_BACKUP_DIR ;; + esac else - echo "Error finding required chromedriver" + printf "\n\nBackup was skipped; the test database remains installed as cdb.\n" fi -printf "\n\nPress enter to proceed restoring original database back to cdb..." -read -s - -yes $db_password | $MY_DIR/cdb_create_db.sh cdb $CDB_BACKUP_DIR - -cd $CURRENT_DIR \ No newline at end of file +cd $CURRENT_DIR diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainInventoryController.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainInventoryController.java index aff3442de..e99c5cf1c 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainInventoryController.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainInventoryController.java @@ -4,6 +4,8 @@ */ package gov.anl.aps.cdb.portal.controllers; +import gov.anl.aps.cdb.common.constants.CdbRole; +import gov.anl.aps.cdb.common.exceptions.CdbException; import gov.anl.aps.cdb.portal.constants.ItemDomainName; import gov.anl.aps.cdb.portal.import_export.import_.helpers.ImportHelperInventory; import gov.anl.aps.cdb.portal.controllers.extensions.ItemCreateWizardController; @@ -12,9 +14,12 @@ import gov.anl.aps.cdb.portal.controllers.extensions.ItemEnforcedPropertiesDomainInventoryController; import gov.anl.aps.cdb.portal.controllers.extensions.ItemMultiEditController; import gov.anl.aps.cdb.portal.controllers.extensions.ItemMultiEditDomainInventoryController; +import gov.anl.aps.cdb.portal.view.objects.InventoryMachineAssignmentSelection; import gov.anl.aps.cdb.portal.controllers.settings.ItemDomainInventorySettings; import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainInventoryControllerUtility; +import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainMachineDesignBaseControllerUtility; import gov.anl.aps.cdb.portal.model.ItemDomainInventoryLazyDataModel; +import gov.anl.aps.cdb.portal.model.ItemDomainMachineDesignTreeNode; import gov.anl.aps.cdb.portal.model.db.beans.ItemDomainInventoryFacade; import gov.anl.aps.cdb.portal.model.db.entities.EntityType; import gov.anl.aps.cdb.portal.model.db.entities.Item; @@ -23,6 +28,8 @@ import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; import gov.anl.aps.cdb.portal.model.db.entities.ItemElementRelationship; import gov.anl.aps.cdb.portal.model.db.entities.ItemProject; +import gov.anl.aps.cdb.portal.model.db.entities.UserInfo; +import gov.anl.aps.cdb.portal.utilities.AuthorizationUtility; import gov.anl.aps.cdb.portal.utilities.SessionUtility; import gov.anl.aps.cdb.portal.view.objects.DomainImportExportInfo; import gov.anl.aps.cdb.portal.view.objects.ImportExportFormatInfo; @@ -52,15 +59,255 @@ public class ItemDomainInventoryController extends ItemDomainInventoryBaseContro private static final String DEFAULT_DOMAIN_NAME = ItemDomainName.inventory.getValue(); private final String DEFAULT_DOMAIN_DERIVED_FROM_ITEM_DOMAIN_NAME = "Catalog"; - private static final Logger logger = LogManager.getLogger(ItemDomainInventoryController.class.getName()); + private static final Logger logger = LogManager.getLogger(ItemDomainInventoryController.class.getName()); @EJB private ItemDomainInventoryFacade itemDomainInventoryFacade; + private final InventoryMachineAssignmentSelection machineAssignmentSelection + = new InventoryMachineAssignmentSelection(); + public boolean isInventory(Item item) { return item instanceof ItemDomainInventory; } + public InventoryMachineAssignmentSelection getMachineAssignmentSelection() { + return machineAssignmentSelection; + } + + public ItemDomainMachineDesignTreeNode getMachineAssignmentTree() { + return machineAssignmentSelection.getTree(); + } + + public ItemDomainMachineDesignTreeNode getSelectedMachineAssignmentNode() { + return machineAssignmentSelection.getSelectedNode(); + } + + public void setSelectedMachineAssignmentNode(ItemDomainMachineDesignTreeNode node) { + machineAssignmentSelection.setSelectedNode(node); + machineAssignmentSelection.setReplacementConfirmation(false); + } + + public boolean isMachineAssignmentInstalled() { + return machineAssignmentSelection.isInstalled(); + } + + public void setMachineAssignmentInstalled(boolean installed) { + machineAssignmentSelection.setInstalled(installed); + } + + public boolean isMachineAssignmentReplacementConfirmation() { + return machineAssignmentSelection.isReplacementConfirmation(); + } + + public void setMachineAssignmentReplacementConfirmation(boolean confirmation) { + machineAssignmentSelection.setReplacementConfirmation(confirmation); + } + + public void prepareMachineAssignment() { + resetMachineAssignmentState(); + ItemDomainMachineDesign assignedMachine = getAssignedMachine(); + if (assignedMachine != null) { + selectMachineAssignment(assignedMachine); + machineAssignmentSelection.setInstalled(assignedMachine.isIsHoused()); + } + } + + protected void selectMachineAssignment(ItemDomainMachineDesign machine) { + machineAssignmentSelection.selectMachine(machine); + } + + public void cancelMachineAssignment() { + resetMachineAssignmentState(); + reloadCurrent(); + } + + public void saveMachineAssignment() { + saveMachineAssignment(false); + } + + public void confirmMachineAssignmentReplacement() { + machineAssignmentSelection.resetReplacementProgress(); + saveMachineAssignment(true); + } + + public void cancelMachineAssignmentReplacement() { + machineAssignmentSelection.setReplacementConfirmation(false); + } + + public void completeMachineAssignmentReplacement() { + finishMachineAssignmentUpdate(); + } + + public Item getSelectedMachineCurrentAssignment() { + ItemDomainMachineDesign machine = machineAssignmentSelection.getSelectedMachine(); + return machine == null ? null : machine.getAssignedItem(); + } + + public Item getSelectedMachineCurrentCatalog() { + return getCatalogItem(getSelectedMachineCurrentAssignment()); + } + + public Item getCurrentInventoryCatalog() { + return getCatalogItem(getCurrent()); + } + + public boolean isCurrentAssignedToMachine() { + return getAssignedMachine() != null; + } + + public boolean isSourceMachineReleaseRequired() { + ItemDomainMachineDesign sourceMachine = getAssignedMachine(); + ItemDomainMachineDesign destinationMachine = machineAssignmentSelection.getSelectedMachine(); + return sourceMachine != null && !sourceMachine.equals(destinationMachine); + } + + public void clearMachineAssignment() { + ItemDomainMachineDesign machine = getAssignedMachine(); + if (machine == null) { + SessionUtility.addInfoMessage("No update", "Inventory item is not assigned to a machine element."); + return; + } + if (!verifyMachineAssignmentPermission(machine)) { + return; + } + try { + logger.info("Clearing inventory {} from machine {}", getCurrent(), machine); + updateMachineAssignment(machine, getCatalogItem(machine.getAssignedItem()), null); + finishMachineAssignmentUpdate(); + } catch (CdbException | RuntimeException ex) { + logger.error("Could not clear inventory machine assignment", ex); + SessionUtility.addErrorMessage("Error", ex.getMessage()); + } + } + + private void saveMachineAssignment(boolean replacementConfirmed) { + ItemDomainMachineDesign machine = machineAssignmentSelection.getSelectedMachine(); + if (machine == null) { + SessionUtility.addErrorMessage("Invalid Selection", "Select a machine element to continue."); + return; + } + if (!verifyMachineAssignmentPermission(machine)) { + return; + } + if (!replacementConfirmed + && requiresCatalogReplacementConfirmation(machine)) { + machineAssignmentSelection.setReplacementConfirmation(true); + SessionUtility.executeRemoteCommand("showMachineAssignmentReplacementConfirmDialog()"); + return; + } + + ItemDomainMachineDesign previousMachine = getAssignedMachine(); + if (previousMachine != null && !previousMachine.equals(machine) + && !verifyMachineAssignmentPermission(previousMachine)) { + return; + } + + boolean previousMachineCleared = false; + boolean previousMachineInstalled = previousMachine != null && previousMachine.isIsHoused(); + try { + if (replacementConfirmed) { + logger.info("Replacing catalog assignment on destination machine {}", machine); + updateMachineAssignment(machine, getCurrentInventoryCatalog(), null); + machineAssignmentSelection.setDestinationPrepared(true); + SessionUtility.addInfoMessage("Machine Assignment", "Updated destination machine catalog assignment."); + } + if (previousMachine != null && !previousMachine.equals(machine)) { + logger.info("Releasing inventory {} from source machine {}", getCurrent(), previousMachine); + updateMachineAssignment(previousMachine, + getCatalogItem(previousMachine.getAssignedItem()), null); + previousMachineCleared = true; + machineAssignmentSelection.setSourceReleased(true); + SessionUtility.addInfoMessage("Machine Assignment", "Released inventory from source machine element."); + reloadCurrent(); + } + logger.info("Assigning inventory {} to destination machine {}", getCurrent(), machine); + updateMachineAssignment(machine, getCurrent(), + machineAssignmentSelection.isInstalled()); + machineAssignmentSelection.setInventoryAssigned(true); + SessionUtility.addInfoMessage("Machine Assignment", "Assigned inventory to destination machine element."); + if (replacementConfirmed) { + ItemDomainMachineDesignController.getInstance().resetListDataModel(); + reloadCurrent(); + } else { + finishMachineAssignmentUpdate(); + } + } catch (CdbException | RuntimeException ex) { + logger.error("Could not update inventory machine assignment", ex); + machineAssignmentSelection.setReplacementError(ex.getMessage()); + if (previousMachineCleared) { + restoreMachineAssignment(previousMachine, previousMachineInstalled); + } + if (!replacementConfirmed) { + SessionUtility.addErrorMessage("Error", ex.getMessage()); + } + } + } + + private boolean verifyMachineAssignmentPermission(ItemDomainMachineDesign machine) { + UserInfo user = SessionUtility.getUser(); + boolean permitted = user != null && (SessionUtility.getRole() == CdbRole.ADMIN + || (AuthorizationUtility.isEntityWriteableByUser(getCurrent(), user) + && AuthorizationUtility.isEntityWriteableByUser(machine, user))); + if (!permitted) { + SessionUtility.addErrorMessage("Error", "Write permission is required for both the inventory item and machine element."); + } + return permitted; + } + + private void restoreMachineAssignment(ItemDomainMachineDesign machine, boolean installed) { + try { + updateMachineAssignment(machine, getCurrent(), installed); + } catch (CdbException | RuntimeException ex) { + logger.error("Could not restore previous inventory machine assignment", ex); + SessionUtility.addErrorMessage("Error", "The previous machine assignment could not be restored."); + } + } + + protected void updateMachineAssignment(ItemDomainMachineDesign machine, Item assignment, + Boolean installed) throws CdbException { + ItemDomainMachineDesignBaseControllerUtility utility = machine.getItemControllerUtility(); + utility.updateAssignedItem(machine, assignment, SessionUtility.getUser(), installed); + utility.update(machine, SessionUtility.getUser()); + } + + protected ItemDomainMachineDesign getAssignedMachine() { + for (ItemElement element : getCurrent().getItemElementMemberList2()) { + Item parent = element.getParentItem(); + if (parent instanceof ItemDomainMachineDesign) { + ItemDomainMachineDesign machine = (ItemDomainMachineDesign) parent; + if (getCurrent().equals(machine.getAssignedItem())) { + return machine; + } + } + } + return null; + } + + protected Item getCatalogItem(Item item) { + return item instanceof ItemDomainInventory ? item.getDerivedFromItem() : item; + } + + protected boolean requiresCatalogReplacementConfirmation(ItemDomainMachineDesign machine) { + Item assignment = machine.getAssignedItem(); + if (assignment == null || assignment.equals(getCurrent())) { + return false; + } + Item assignedCatalog = getCatalogItem(assignment); + return assignedCatalog == null || !assignedCatalog.equals(getCurrentInventoryCatalog()); + } + + protected void finishMachineAssignmentUpdate() { + ItemDomainMachineDesignController.getInstance().resetListDataModel(); + resetMachineAssignmentState(); + reloadCurrent(); + SessionUtility.executeRemoteCommand("machineAssignmentUpdateSuccess()"); + } + + public void resetMachineAssignmentState() { + machineAssignmentSelection.reset(); + } + @Override protected ItemDomainInventoryFacade getEntityDbFacade() { return itemDomainInventoryFacade; diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainMachineDesignIOCController.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainMachineDesignIOCController.java index 02f07754a..4bdd144a9 100644 --- a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainMachineDesignIOCController.java +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/controllers/ItemDomainMachineDesignIOCController.java @@ -5,6 +5,7 @@ package gov.anl.aps.cdb.portal.controllers; import gov.anl.aps.cdb.common.exceptions.CdbException; +import gov.anl.aps.cdb.portal.view.objects.MachineDesignTreeSelection; import gov.anl.aps.cdb.portal.controllers.settings.ItemDomainMachineDesignIOCSettings; import gov.anl.aps.cdb.portal.controllers.utilities.ItemDomainMachineDesignIOCControllerUtility; import gov.anl.aps.cdb.portal.controllers.utilities.ItemElementControllerUtility; @@ -47,8 +48,7 @@ public class ItemDomainMachineDesignIOCController extends ItemDomainMachineDesig + "2. Power cycling may interrupt system communications temporarily\n" + "3. Verify all dependent systems are in safe state before proceeding"; - private ItemDomainMachineDesignTreeNode parentMachineSelectionTreeNode; - private ItemDomainMachineDesignTreeNode parentMachineSelectedTreeNode = null; + private final MachineDesignTreeSelection parentMachineSelection = new MachineDesignTreeSelection(); public static ItemDomainMachineDesignIOCController getInstance() { return (ItemDomainMachineDesignIOCController) SessionUtility.findBean(CONTROLLER_NAMED); @@ -136,20 +136,27 @@ public boolean getDisplayCreatedFromTemplateForCurrent() { @Override public String prepareClone(ItemDomainMachineDesign entity) { - // Reset variables - parentMachineSelectionTreeNode = null; + parentMachineSelection.reset(); return super.prepareClone(entity); } @Override public String prepareCreate() { - // Reset variables - parentMachineSelectionTreeNode = null; + parentMachineSelection.reset(); return super.prepareCreate(); } + public void prepareMachineParentSelection() { + ItemDomainMachineDesign current = getCurrent(); + // Unsaved items use the parent selected in the UI because no relationship exists yet. + ItemDomainMachineDesign parentMachine = current.getId() == null + ? current.getParentMachineDesignPlaceholder() + : current.getParentMachineDesign(); + parentMachineSelection.selectMachine(parentMachine); + } + public String clearMachineParent() { ItemDomainMachineDesign current = getCurrent(); UserInfo user = SessionUtility.getUser(); @@ -202,33 +209,18 @@ private void prepareSetParentMachineAttributes() { } public ItemDomainMachineDesignTreeNode getParentMachineSelectionTreeNode() { - if (parentMachineSelectionTreeNode == null) { - ItemDomainMachineDesignController instance = ItemDomainMachineDesignController.getInstance(); - parentMachineSelectionTreeNode = instance.loadMachineDesignRootTreeNode(); - - } - return parentMachineSelectionTreeNode; + return parentMachineSelection.getTree(); } public ItemDomainMachineDesignTreeNode getParentMachineSelectedTreeNode() { - return parentMachineSelectedTreeNode; + return parentMachineSelection.getSelectedNode(); } - public void setParentMachineSelectedTreeNode(ItemDomainMachineDesignTreeNode parentMachineSelectedTreeNode) { - this.parentMachineSelectedTreeNode = parentMachineSelectedTreeNode; - - if (parentMachineSelectedTreeNode != null) { - ItemElement machineElement = (ItemElement) parentMachineSelectedTreeNode.getData(); - - if (machineElement.getId() == null) { - return; - } - - ItemDomainMachineDesign current = getCurrent(); - ItemDomainMachineDesign parentItem = (ItemDomainMachineDesign) machineElement.getContainedItem(); - - current.setParentMachineDesignPlaceholder(parentItem); - + public void setParentMachineSelectedTreeNode(ItemDomainMachineDesignTreeNode selectedNode) { + parentMachineSelection.setSelectedNode(selectedNode); + ItemDomainMachineDesign selectedMachine = parentMachineSelection.getSelectedMachine(); + if (selectedMachine != null) { + getCurrent().setParentMachineDesignPlaceholder(selectedMachine); } } diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/view/objects/InventoryMachineAssignmentSelection.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/view/objects/InventoryMachineAssignmentSelection.java new file mode 100644 index 000000000..feead7ac6 --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/view/objects/InventoryMachineAssignmentSelection.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.portal.view.objects; + +/** + * Inventory-specific state layered on reusable machine-tree selection. + */ +public class InventoryMachineAssignmentSelection extends MachineDesignTreeSelection { + + private boolean installed = true; + private boolean replacementConfirmation; + private boolean destinationPrepared; + private boolean sourceReleased; + private boolean inventoryAssigned; + private String replacementError; + + public boolean isInstalled() { + return installed; + } + + public void setInstalled(boolean installed) { + this.installed = installed; + } + + public boolean isReplacementConfirmation() { + return replacementConfirmation; + } + + public void setReplacementConfirmation(boolean replacementConfirmation) { + this.replacementConfirmation = replacementConfirmation; + } + + public boolean isDestinationPrepared() { + return destinationPrepared; + } + + public void setDestinationPrepared(boolean destinationPrepared) { + this.destinationPrepared = destinationPrepared; + } + + public boolean isSourceReleased() { + return sourceReleased; + } + + public void setSourceReleased(boolean sourceReleased) { + this.sourceReleased = sourceReleased; + } + + public boolean isInventoryAssigned() { + return inventoryAssigned; + } + + public void setInventoryAssigned(boolean inventoryAssigned) { + this.inventoryAssigned = inventoryAssigned; + } + + public String getReplacementError() { + return replacementError; + } + + public void setReplacementError(String replacementError) { + this.replacementError = replacementError; + } + + public void resetReplacementProgress() { + destinationPrepared = false; + sourceReleased = false; + inventoryAssigned = false; + replacementError = null; + } + + @Override + public void reset() { + super.reset(); + installed = true; + replacementConfirmation = false; + resetReplacementProgress(); + } +} diff --git a/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/view/objects/MachineDesignTreeSelection.java b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/view/objects/MachineDesignTreeSelection.java new file mode 100644 index 000000000..f6427af3e --- /dev/null +++ b/src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/view/objects/MachineDesignTreeSelection.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) UChicago Argonne, LLC. All rights reserved. + * See LICENSE file. + */ +package gov.anl.aps.cdb.portal.view.objects; + +import gov.anl.aps.cdb.portal.controllers.ItemDomainMachineDesignController; +import gov.anl.aps.cdb.portal.model.ItemDomainMachineDesignTreeNode; +import gov.anl.aps.cdb.portal.model.db.entities.Item; +import gov.anl.aps.cdb.portal.model.db.entities.ItemDomainMachineDesign; +import gov.anl.aps.cdb.portal.model.db.entities.ItemElement; + +/** + * Reusable state for selecting one item from the machine-design tree. + */ +public class MachineDesignTreeSelection { + + private ItemDomainMachineDesignTreeNode tree; + private ItemDomainMachineDesignTreeNode selectedNode; + + public ItemDomainMachineDesignTreeNode getTree() { + if (tree == null) { + tree = ItemDomainMachineDesignController.getInstance().loadMachineDesignRootTreeNode(); + } + return tree; + } + + public ItemDomainMachineDesignTreeNode getSelectedNode() { + return selectedNode; + } + + public void setSelectedNode(ItemDomainMachineDesignTreeNode selectedNode) { + this.selectedNode = selectedNode; + } + + public void selectMachine(ItemDomainMachineDesign machine) { + reset(); + if (machine != null) { + selectedNode = (ItemDomainMachineDesignTreeNode) + ItemDomainMachineDesignController.expandToSpecificMachineDesignItem(getTree(), machine); + if (selectedNode != null) { + selectedNode.setSelected(true); + } + } + } + + public ItemDomainMachineDesign getSelectedMachine() { + if (selectedNode == null || selectedNode.getData() == null) { + return null; + } + ItemElement element = (ItemElement) selectedNode.getData(); + Item item = element.getContainedItem(); + return item instanceof ItemDomainMachineDesign ? (ItemDomainMachineDesign) item : null; + } + + public void reset() { + tree = null; + selectedNode = null; + } +} diff --git a/src/java/CdbWebPortal/web/views/itemDomainInventory/private/componentInventoryViewPanelGrid.xhtml b/src/java/CdbWebPortal/web/views/itemDomainInventory/private/componentInventoryViewPanelGrid.xhtml index 90ee1acd7..cc50387c2 100644 --- a/src/java/CdbWebPortal/web/views/itemDomainInventory/private/componentInventoryViewPanelGrid.xhtml +++ b/src/java/CdbWebPortal/web/views/itemDomainInventory/private/componentInventoryViewPanelGrid.xhtml @@ -14,7 +14,16 @@ See LICENSE file. - + + + + + + + diff --git a/src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentDialog.xhtml b/src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentDialog.xhtml new file mode 100644 index 000000000..2511a1f76 --- /dev/null +++ b/src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentDialog.xhtml @@ -0,0 +1,61 @@ + + + + + + + + + + + + Installed items use the machine location. Planned items retain their current location. + + + + + + + + + + + + + + + + + diff --git a/src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentReplacementConfirmDialog.xhtml b/src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentReplacementConfirmDialog.xhtml new file mode 100644 index 000000000..490399f69 --- /dev/null +++ b/src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentReplacementConfirmDialog.xhtml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentTree.xhtml b/src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentTree.xhtml new file mode 100644 index 000000000..44c252a6b --- /dev/null +++ b/src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentTree.xhtml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/src/java/CdbWebPortal/web/views/itemDomainInventory/view.xhtml b/src/java/CdbWebPortal/web/views/itemDomainInventory/view.xhtml index ecce1347f..74ee3ac6c 100644 --- a/src/java/CdbWebPortal/web/views/itemDomainInventory/view.xhtml +++ b/src/java/CdbWebPortal/web/views/itemDomainInventory/view.xhtml @@ -65,6 +65,7 @@ See LICENSE file. + diff --git a/src/java/CdbWebPortal/web/views/itemDomainMachineDesignBase/private/itemDomainMachineDesignTreeSelection.xhtml b/src/java/CdbWebPortal/web/views/itemDomainMachineDesignBase/private/itemDomainMachineDesignTreeSelection.xhtml new file mode 100644 index 000000000..f28e2f6b4 --- /dev/null +++ b/src/java/CdbWebPortal/web/views/itemDomainMachineDesignBase/private/itemDomainMachineDesignTreeSelection.xhtml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/java/CdbWebPortal/web/views/itemDomainMachineDesignBase/private/templates/itemDomainMachineDesignListObjectTemplate.xhtml b/src/java/CdbWebPortal/web/views/itemDomainMachineDesignBase/private/templates/itemDomainMachineDesignListObjectTemplate.xhtml index 8599216df..74d0bb9e5 100644 --- a/src/java/CdbWebPortal/web/views/itemDomainMachineDesignBase/private/templates/itemDomainMachineDesignListObjectTemplate.xhtml +++ b/src/java/CdbWebPortal/web/views/itemDomainMachineDesignBase/private/templates/itemDomainMachineDesignListObjectTemplate.xhtml @@ -30,6 +30,11 @@ See LICENSE file. + + @@ -209,8 +214,7 @@ See LICENSE file. value="#{mdTreeListObject != null ? mdTreeListObject.assignedItem : itemTreeListObject}"/> diff --git a/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/create.xhtml b/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/create.xhtml index 19959d4a9..8973dbc56 100644 --- a/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/create.xhtml +++ b/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/create.xhtml @@ -28,7 +28,10 @@ See LICENSE file. - + diff --git a/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/private/iocMachineParentSelectionTree.xhtml b/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/private/iocMachineParentSelectionTree.xhtml index e70a487e4..15b801683 100644 --- a/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/private/iocMachineParentSelectionTree.xhtml +++ b/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/private/iocMachineParentSelectionTree.xhtml @@ -5,35 +5,10 @@ See LICENSE file. --> - - - - - - - - - - - - - - - - - - - - - - - - - + xmlns:ui="http://java.sun.com/jsf/facelets"> + + + + + - - diff --git a/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/view.xhtml b/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/view.xhtml index c4ed6337b..228510b3b 100644 --- a/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/view.xhtml +++ b/src/java/CdbWebPortal/web/views/itemDomainMachineDesignIOC/view.xhtml @@ -35,7 +35,9 @@ See LICENSE file. + action="#{itemDomainMachineDesignIOCController.prepareMachineParentSelection()}" + update="@form:iocMachineParentSelectionDialog" + oncomplete="PF('iocMachineParentSelectionDialogWidget').show(); scrollMachineSelectionToSelectedcreateIocMachineSelectionDialog()"/> diff --git a/src/java/CdbWebPortal/web/views/locatableItem/private/locatableItemListLocationObject.xhtml b/src/java/CdbWebPortal/web/views/locatableItem/private/locatableItemListLocationObject.xhtml index acad52844..54b1bae96 100644 --- a/src/java/CdbWebPortal/web/views/locatableItem/private/locatableItemListLocationObject.xhtml +++ b/src/java/CdbWebPortal/web/views/locatableItem/private/locatableItemListLocationObject.xhtml @@ -46,9 +46,13 @@ See LICENSE file. + + - + rendered="#{rendredHousing}" + filterBy="#{housingItem.name}" + filterable="#{filterable}" + filterMatchMode="contains">