From 07fc2b954a906d23082c97919c707bb7c34c00c4 Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Mon, 26 Oct 2026 15:30:59 -0500 Subject: [PATCH 01/34] Add a standardized way of managing a machine tree for selection of a new item. --- .../objects/MachineDesignTreeSelection.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/view/objects/MachineDesignTreeSelection.java 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; + } +} From 63ce077babf5bdc0abe93e44b93d6873eba48b2b Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Tue, 27 Oct 2026 08:21:29 -0500 Subject: [PATCH 02/34] Add standard scroll panel for machine selection. --- ...itemDomainMachineDesignTreeSelection.xhtml | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/java/CdbWebPortal/web/views/itemDomainMachineDesignBase/private/itemDomainMachineDesignTreeSelection.xhtml 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..af9d1ad18 --- /dev/null +++ b/src/java/CdbWebPortal/web/views/itemDomainMachineDesignBase/private/itemDomainMachineDesignTreeSelection.xhtml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + From 7710654f0e88686ae3cb6cd3903f62232326835c Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Tue, 27 Oct 2026 08:22:56 -0500 Subject: [PATCH 03/34] Utilize the new standardized functionality with ioc machine parent selection. Also now selects and scolls existing parent. --- .../ItemDomainMachineDesignIOCController.java | 48 ++++++++----------- .../itemDomainMachineDesignIOC/create.xhtml | 5 +- .../iocMachineParentSelectionTree.xhtml | 37 +++----------- .../itemDomainMachineDesignIOC/view.xhtml | 4 +- 4 files changed, 33 insertions(+), 61 deletions(-) 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/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()"/> From 6ad240c9d92f9b07be74916b48b6d26c7dbc9d12 Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Tue, 27 Oct 2026 09:54:20 -0500 Subject: [PATCH 04/34] Add model that drives the UI for the machine selection when updating an inventory assignment. --- .../InventoryMachineAssignmentSelection.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/java/CdbWebPortal/src/java/gov/anl/aps/cdb/portal/view/objects/InventoryMachineAssignmentSelection.java 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(); + } +} From 35e4688575b384c615f93949c2a65a8350edd68b Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Tue, 27 Oct 2026 09:58:03 -0500 Subject: [PATCH 05/34] Add functionality that allows updating the housing assignment from the inventory details page. ItemDomainInventoryController now manages machine-assignment selection, permissions, Installed/Planned state, source release, clear-to-catalog, destination replacement confirmation/progress, persistence, rollback. --- .../ItemDomainInventoryController.java | 245 +++++++++++++++++- 1 file changed, 244 insertions(+), 1 deletion(-) 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..6c9fc1270 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,251 @@ 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) { + machineAssignmentSelection.selectMachine(assignedMachine); + machineAssignmentSelection.setInstalled(assignedMachine.isIsHoused()); + } + } + + 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."); + } + } + + private 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()); + } + + private 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; + } + + private Item getCatalogItem(Item item) { + return item instanceof ItemDomainInventory ? item.getDerivedFromItem() : item; + } + + private 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()); + } + + private void finishMachineAssignmentUpdate() { + ItemDomainMachineDesignController.getInstance().resetListDataModel(); + resetMachineAssignmentState(); + reloadCurrent(); + SessionUtility.executeRemoteCommand("machineAssignmentUpdateSuccess()"); + } + + public void resetMachineAssignmentState() { + machineAssignmentSelection.reset(); + } + @Override protected ItemDomainInventoryFacade getEntityDbFacade() { return itemDomainInventoryFacade; From 704b29ab87a0dfda591b4f554195addabaadfa2f Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Tue, 27 Oct 2026 09:59:16 -0500 Subject: [PATCH 06/34] Add supporting views for the inventory housing selection. --- .../componentInventoryViewPanelGrid.xhtml | 11 ++- ...mainInventoryMachineAssignmentDialog.xhtml | 61 +++++++++++++++++ ...neAssignmentReplacementConfirmDialog.xhtml | 68 +++++++++++++++++++ ...DomainInventoryMachineAssignmentTree.xhtml | 19 ++++++ .../web/views/itemDomainInventory/view.xhtml | 1 + .../locatableItemViewPanelGridObject.xhtml | 13 ++-- 6 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentDialog.xhtml create mode 100644 src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentReplacementConfirmDialog.xhtml create mode 100644 src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentTree.xhtml 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..968e59160 --- /dev/null +++ b/src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentTree.xhtml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + 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/locatableItem/private/locatableItemViewPanelGridObject.xhtml b/src/java/CdbWebPortal/web/views/locatableItem/private/locatableItemViewPanelGridObject.xhtml index dc0331f04..72b87b9cb 100644 --- a/src/java/CdbWebPortal/web/views/locatableItem/private/locatableItemViewPanelGridObject.xhtml +++ b/src/java/CdbWebPortal/web/views/locatableItem/private/locatableItemViewPanelGridObject.xhtml @@ -118,10 +118,15 @@ See LICENSE file. - - - - + + + + + + + + + From 5f232d27a34300c83697d8f4f8ef9cf9f6003a3b Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Tue, 27 Oct 2026 10:49:23 -0500 Subject: [PATCH 07/34] Add a new top level to test swapping catalog item --- db/sql/test/populate_entity_info.sql | 5 ++++- db/sql/test/populate_item.sql | 4 +++- db/sql/test/populate_item_element.sql | 5 ++++- db/sql/test/populate_item_element_history.sql | 6 +++++- db/sql/test/populate_item_item_project.sql | 4 +++- db/sql/test/populate_log.sql | 6 +++++- db/sql/test/populate_system_log.sql | 6 +++++- 7 files changed, 29 insertions(+), 7 deletions(-) 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; From 1a65181dc6a5873b56051308a957bb77eefe0b8e Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Tue, 27 Oct 2026 10:49:47 -0500 Subject: [PATCH 08/34] Add a new inventory assignment acceptance test. --- .../cdbSeleniumModules/inventory.py | 131 +++++++++++++++++- .../PythonSeleniumTest/gui_test.py | 3 + 2 files changed, 130 insertions(+), 4 deletions(-) mode change 100644 => 100755 tools/developer_tools/portal_testing/PythonSeleniumTest/gui_test.py diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/cdbSeleniumModules/inventory.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/cdbSeleniumModules/inventory.py index 2109243f2..bde6b77f9 100644 --- a/tools/developer_tools/portal_testing/PythonSeleniumTest/cdbSeleniumModules/inventory.py +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/cdbSeleniumModules/inventory.py @@ -1,3 +1,5 @@ +from selenium.webdriver.common.by import By + from cdbSeleniumModules.itemBase import ItemBase @@ -16,10 +18,19 @@ class Inventory(ItemBase): IMPORT_FILE_NAME = "Inventory Import.xlsx" + ASSIGNMENT_DIALOG_ID = VIEW_FORM_NAME + ':machineAssignmentDialog' + ASSIGNMENT_TREE_ID = VIEW_FORM_NAME + ':itemMachineDesignListDataTableinventoryMachineAssignment' + REPLACEMENT_DIALOG_ID = VIEW_FORM_NAME + ':machineAssignmentReplacementConfirmDialog' + + TEST_MACHINE = ['Test Machine'] + TEST_MACHINE_COMPONENT_1 = ['Test Machine', 'Component 1'] + TEST_MACHINE_COMPONENT_2 = ['Test Machine', 'Another Needed Component'] + TEST_REPLACEMENT_DESTINATION = ['Another Top Level', 'Inventory Assignment Test Destination'] + def navigate_to_inventory_list(self): self._navigate_to_dropdown('inventoryDropdownButton', 'componentInstanceButton', '%s/list' % self.VIEW_BASE_NAME) - def test_inventory_pages(self): + def test_inventory_pages(self): self._click_on_xpath(self.DATA_TABLE_XPATH_FORMULA % (self.LIST_FORM_NAME, self.ENTITY_TYPE_NAME, self.COLUMN_IDX)) self._wait_for_url_contains('%s/view' % self.VIEW_BASE_NAME) @@ -29,7 +40,7 @@ def test_inventory_pages(self): self._click_on_id('%s:%sEditViewButton' % (self.EDIT_FORM_NAME, self.ENTITY_TYPE_NAME)) self._wait_for_url_contains('%s/view' % self.VIEW_BASE_NAME) - def test_inventory_detail_page(self, test): + def test_inventory_detail_page(self, test): self._click_on_xpath(self.DATA_TABLE_XPATH_FORMULA % (self.LIST_FORM_NAME, self.ENTITY_TYPE_NAME, self.COLUMN_IDX)) self._wait_for_url_contains('%s/view' % self.VIEW_BASE_NAME) @@ -37,9 +48,121 @@ def test_inventory_detail_page(self, test): self._clear_notifications() self._add_property_to_item(test, self.VIEW_FORM_NAME, self.ENTITY_TYPE_NAME, "Inventory Property.") self._add_image_to_item(self.VIEW_FORM_NAME, self.ENTITY_TYPE_NAME) - + + def test_machine_assignment_acceptance(self, test): + """Acceptance coverage for inventory-side machine assignment.""" + self._open_inventory_by_id(41) + + # Selection and Installed/Planned state are available from Housing edit. + self._open_machine_assignment() + test.assertTrue(self._wait_for_visible_id(self.ASSIGNMENT_DIALOG_ID).is_displayed()) + install_button = self._wait_for_id(self.VIEW_FORM_NAME + ':machineAssignmentInstalled') + test.assertIn('Installed', install_button.text) + + # Select an empty destination and save as Planned; persistence/view refresh + # are verified by reopening the dialog and checking its restored state. + self._select_machine(self.TEST_MACHINE_COMPONENT_1) + install_button.click() + save_button = self._wait_for_xpath( + "//*[@id='%s:machineAssignmentDialogFooter']//button[span[text()='Save']]" % self.VIEW_FORM_NAME) + save_button.click() + self._wait_for_invisible_by_id(self.ASSIGNMENT_DIALOG_ID) + self._open_machine_assignment() + selected = self._wait_for_xpath( + "//*[@id='%s']//tr[@aria-selected='true']" % self.ASSIGNMENT_TREE_ID) + test.assertIn(self.TEST_MACHINE_COMPONENT_1[-1], selected.text) + test.assertIn('Planned', self._wait_for_id(self.VIEW_FORM_NAME + ':machineAssignmentInstalled').text) + + # Moving to another destination releases the source assignment first. + self._select_machine(self.TEST_MACHINE_COMPONENT_2) + save_button = self._wait_for_xpath( + "//*[@id='%s:machineAssignmentDialogFooter']//button[span[text()='Save']]" % self.VIEW_FORM_NAME) + save_button.click() + self._wait_for_invisible_by_id(self.ASSIGNMENT_DIALOG_ID) + self._open_machine_assignment() + selected = self._wait_for_xpath( + "//*[@id='%s']//tr[@aria-selected='true']" % self.ASSIGNMENT_TREE_ID) + test.assertIn(self.TEST_MACHINE_COMPONENT_2[-1], selected.text) + + # Clear swaps inventory for its catalog and removes Housing assignment. + clear_button = self._wait_for_xpath( + "//*[@id='%s:machineAssignmentDialogFooter']//button[span[text()='Clear']]" % self.VIEW_FORM_NAME) + clear_button.click() + self._wait_for_invisible_by_id(self.ASSIGNMENT_DIALOG_ID) + self._open_machine_assignment() + test.assertFalse(self._has_selected_machine()) + + # Different-catalog destination requires confirmation and completes all steps. + self._select_machine(self.TEST_REPLACEMENT_DESTINATION) + save_button = self._wait_for_xpath( + "//*[@id='%s:machineAssignmentDialogFooter']//button[span[text()='Save']]" % self.VIEW_FORM_NAME) + save_button.click() + confirm_dialog = self._wait_for_visible_id(self.REPLACEMENT_DIALOG_ID) + test.assertTrue(confirm_dialog.is_displayed()) + test.assertIn('Clear destination assignment and update catalog', confirm_dialog.text) + test.assertIn('Assign inventory to destination machine element', confirm_dialog.text) + + self._click_on_xpath( + "//*[@id='%s']//button[span[text()='Clear Destination and Assign']]" % self.REPLACEMENT_DIALOG_ID) + done_button = self._wait_for_clickable_xpath( + "//*[@id='%s']//button[span[text()='Done']]" % self.REPLACEMENT_DIALOG_ID) + completed_icons = self.driver.find_elements( + By.CSS_SELECTOR, '#%s .fa-check-square-o' % self.REPLACEMENT_DIALOG_ID.replace(':', '\\:')) + test.assertEqual(2, len(completed_icons), 'Destination preparation and assignment should complete') + done_button.click() + self._wait_for_invisible_by_id(self.ASSIGNMENT_DIALOG_ID) + + # Reopen to verify replacement persistence, then clear for test cleanup. + self._open_machine_assignment() + selected = self._wait_for_xpath( + "//*[@id='%s']//tr[@aria-selected='true']" % self.ASSIGNMENT_TREE_ID) + test.assertIn(self.TEST_REPLACEMENT_DESTINATION[-1], selected.text) + clear_button = self._wait_for_xpath( + "//*[@id='%s:machineAssignmentDialogFooter']//button[span[text()='Clear']]" % self.VIEW_FORM_NAME) + clear_button.click() + self._wait_for_invisible_by_id(self.ASSIGNMENT_DIALOG_ID) + + def _open_inventory_by_id(self, inventory_id): + self.driver.get('%s/views/%s/view.xhtml?id=%s' % ( + self.driver.current_url.split('/views/')[0], self.VIEW_BASE_NAME, inventory_id)) + self._wait_for_url_contains('%s/view' % self.VIEW_BASE_NAME) + + def _open_machine_assignment(self): + self._wait_for_id_and_click(self.VIEW_FORM_NAME + ':machineAssignmentEditButton') + self._wait_for_visible_id(self.ASSIGNMENT_DIALOG_ID) + + def _select_machine(self, hierarchy): + """Expand the known test hierarchy and select its exact leaf.""" + for name in hierarchy[:-1]: + row = self._wait_for_machine_row(name) + toggler = row.find_element(By.CSS_SELECTOR, '.ui-treetable-toggler') + if 'ui-icon-triangle-1-e' in toggler.get_attribute('class'): + toggler.click() + self._wait_for_machine_row(hierarchy[-1]) + + self._wait_for_machine_row(hierarchy[-1]).click() + + def _wait_for_machine_row(self, name): + row_xpath = ("//*[@id='%s']//tr[.//span[normalize-space(text())=%s]]" + % (self.ASSIGNMENT_TREE_ID, self._xpath_literal(name))) + return self._wait_for_clickable_xpath(row_xpath) + + @staticmethod + def _xpath_literal(value): + if "'" not in value: + return "'%s'" % value + if '"' not in value: + return '"%s"' % value + parts = value.split("'") + return "concat(%s)" % ", \"'\", ".join("'%s'" % part for part in parts) + + def _has_selected_machine(self): + selected = self.driver.find_elements( + By.XPATH, "//*[@id='%s']//tr[@aria-selected='true']" % self.ASSIGNMENT_TREE_ID) + return len(selected) > 0 + def test_import_create(self, test): self._wait_for_id_and_click('%s:%sImportButton' % (self.LIST_FORM_NAME, self.ENTITY_TYPE_NAME)) table_results = self._import_navigate_to_verification_data_table(self.IMPORT_FORM_NAME, self.IMPORT_FILE_NAME) test.assertEqual(len(table_results), 25, msg='25 items were imported in the spreadsheet') - self._import_complete(self.IMPORT_FORM_NAME, self.VIEW_BASE_NAME) \ No newline at end of file + self._import_complete(self.IMPORT_FORM_NAME, self.VIEW_BASE_NAME) diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/gui_test.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/gui_test.py old mode 100644 new mode 100755 index 9d667a3fa..b1e2400aa --- a/tools/developer_tools/portal_testing/PythonSeleniumTest/gui_test.py +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/gui_test.py @@ -167,6 +167,9 @@ def test_inventory_detail_page(self): self.inventory.navigate_to_inventory_list() self.inventory.test_inventory_detail_page(self) + def test_inventory_machine_assignment(self): + self.inventory.test_machine_assignment_acceptance(self) + def test_inventory_import(self): self.inventory.navigate_to_inventory_list() self.inventory.test_import_create(self) From faa6cc30c3354cf13c6118a521ee1c99b5539438 Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Tue, 27 Oct 2026 10:50:06 -0500 Subject: [PATCH 09/34] Improve test reliablity when panels might be collapsed --- .../cdbSeleniumModuleBase.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/cdbSeleniumModules/cdbSeleniumModuleBase.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/cdbSeleniumModules/cdbSeleniumModuleBase.py index 905a91936..e20297cba 100644 --- a/tools/developer_tools/portal_testing/PythonSeleniumTest/cdbSeleniumModules/cdbSeleniumModuleBase.py +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/cdbSeleniumModules/cdbSeleniumModuleBase.py @@ -8,7 +8,7 @@ import time from selenium import webdriver -from selenium.common.exceptions import StaleElementReferenceException, NoSuchElementException +from selenium.common.exceptions import ElementNotInteractableException, NoSuchElementException, StaleElementReferenceException from selenium.webdriver import ActionChains from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC @@ -50,7 +50,10 @@ def _clear_notifications(self): action.move_to_element(growl_container) action.perform() time.sleep(.1) - growl_close.click() + try: + growl_close.click() + except ElementNotInteractableException: + self._click_using_javascript(growl_close) time.sleep(0.5) growl_container = next_growl @@ -293,9 +296,17 @@ def _export(self, form_name, result_file_name, test, has_levels=False): test.verify_file_downloaded(result_file_name) + def _expand_panel(self, form_name, panel_id): + panel = self._wait_for_id('%s:%s' % (form_name, panel_id)) + content = panel.find_element(by=By.CSS_SELECTOR, value='.ui-panel-content') + if not content.is_displayed(): + self._click_on_id('%s:%s_toggler' % (form_name, panel_id)) + WebDriverWait(self.driver, CdbSeleniumModuleBase.WAIT_FOR_ELEMENT_TIMEOUT).until( + lambda driver: content.is_displayed()) + def _add_log_to_item(self, form_name, entity_name, log_entry, needs_toggler=True): if needs_toggler: - self._click_on_id('%s:%sViewLogEntriesPanel_toggler' % (form_name, entity_name)) + self._expand_panel(form_name, '%sViewLogEntriesPanel' % entity_name) self._wait_for_id_and_click('%s:%sLogAddButton' % (form_name, entity_name)) logEntry = self._wait_for_id('%s:logEntryValue' % form_name) @@ -307,12 +318,11 @@ def _add_log_to_item(self, form_name, entity_name, log_entry, needs_toggler=True def _add_property_to_item(self, test, form_name, entity_name, prop_value_text, needs_toggler=True): if needs_toggler: - toggle_id = '%s:%sViewPropertiesPanel_toggler' % (form_name, entity_name) - self._click_on_id(toggle_id) + self._expand_panel(form_name, '%sViewPropertiesPanel' % entity_name) add_btn_id = '%s:%sPropertyAddButton' % (form_name, entity_name) self._wait_for_id_and_click(add_btn_id) - categorySelectionXpath = "//div[@id='%s:filterViewItemCategorySelection']/div[2]/ul/li" % form_name + categorySelectionXpath = "//*[@id='%s:filterViewItemCategorySelection']//li[1]" % form_name categorySel = self._wait_for_visible_xpath(categorySelectionXpath) categorySel.click() @@ -339,8 +349,7 @@ def _add_property_to_item(self, test, form_name, entity_name, prop_value_text, n def _add_image_to_item(self, form_name, entity_name, sample_file_name='AnlLogo.png', needs_toggler=True): if needs_toggler: - toggle_id = "%s:%sViewGalleryPanel_toggler" % (form_name, entity_name) - self._click_on_id(toggle_id) + self._expand_panel(form_name, '%sViewGalleryPanel' % entity_name) add_btn_id = "%s:%sPropertyAddImagesCommandButton" % (form_name, entity_name) self._wait_for_id_and_click(add_btn_id) From 808cf758f024ce29a1a41b3ec38a6bd4543c43c1 Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Tue, 27 Oct 2026 11:03:14 -0500 Subject: [PATCH 10/34] add docs about the new acceptance test --- ...INVENTORY_MACHINE_ASSIGNMENT_ACCEPTANCE.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/docs/INVENTORY_MACHINE_ASSIGNMENT_ACCEPTANCE.md diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/docs/INVENTORY_MACHINE_ASSIGNMENT_ACCEPTANCE.md b/tools/developer_tools/portal_testing/PythonSeleniumTest/docs/INVENTORY_MACHINE_ASSIGNMENT_ACCEPTANCE.md new file mode 100644 index 000000000..2dd544e28 --- /dev/null +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/docs/INVENTORY_MACHINE_ASSIGNMENT_ACCEPTANCE.md @@ -0,0 +1,20 @@ +# Inventory Machine Assignment Acceptance Coverage + +Automated by `CdbPortalFunctionalTestSuite.test_inventory_machine_assignment` using the test database fixtures. + +## Automated scenarios + +- Open Housing edit and verify the machine-selection dialog and Installed/Planned control. +- Assign inventory as **Planned**, reopen the dialog, and verify the selected machine and state persisted. +- Move assigned inventory to another machine and verify the destination becomes selected after refresh (source release and view refresh). +- Clear the assignment and verify no machine remains selected (inventory is replaced by its catalog on the machine). +- Expand `Another Top Level`, select `Inventory Assignment Test Destination`, confirm the different-catalog replacement, verify applicable progress steps complete, reopen to verify persistence, and clear for cleanup. + +## Manual acceptance scenarios + +These require fixture/user manipulation or an injected persistence failure and remain manual: + +- Verify assignment is rejected unless the user can write the inventory, source machine, and destination machine. +- Force destination persistence to fail after source release and verify the source inventory assignment and Installed/Planned state are restored. + +Logging is intentionally not asserted as acceptance behavior. From 80bd1f3bd8c61549d557c1425cfcfc9285bf2636 Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Tue, 27 Oct 2026 11:12:11 -0500 Subject: [PATCH 11/34] Improve test reliablity --- .../cdbSeleniumModules/cdbSeleniumModuleBase.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/cdbSeleniumModules/cdbSeleniumModuleBase.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/cdbSeleniumModules/cdbSeleniumModuleBase.py index e20297cba..6b6e8b406 100644 --- a/tools/developer_tools/portal_testing/PythonSeleniumTest/cdbSeleniumModules/cdbSeleniumModuleBase.py +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/cdbSeleniumModules/cdbSeleniumModuleBase.py @@ -8,7 +8,7 @@ import time from selenium import webdriver -from selenium.common.exceptions import ElementNotInteractableException, NoSuchElementException, StaleElementReferenceException +from selenium.common.exceptions import ElementClickInterceptedException, ElementNotInteractableException, NoSuchElementException, StaleElementReferenceException from selenium.webdriver import ActionChains from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC @@ -24,6 +24,8 @@ class CdbSeleniumModuleBase: WAIT_FOR_ELEMENT_TIMEOUT = 10 VISIBLE_ID_TIMEOUT = 10 + CLICK_RETRY_COUNT = 5 + CLICK_RETRY_DELAY = 0.1 LOADING_DIALOG_ID = 'loadingDialog' def __init__(self, driver): @@ -92,8 +94,16 @@ def _find_by_xpath(self, xpath): return self.driver.find_element(by=By.XPATH, value=xpath) def _wait_for_id_and_click(self, id): - element = self._wait_for_id(id) - element.click() + for attempt in range(self.CLICK_RETRY_COUNT): + try: + element = self._wait_for_clickable_id(id) + element.click() + return + except (ElementClickInterceptedException, StaleElementReferenceException): + if attempt < self.CLICK_RETRY_COUNT - 1: + time.sleep(self.CLICK_RETRY_DELAY) + continue + raise def _wait_for_visible_id(self, id): return self._wait_for_visible(By.ID, id) From 01db68d46392228dd8eb37e24c292ede30e6af97 Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Tue, 27 Oct 2026 11:43:03 -0500 Subject: [PATCH 12/34] Resolve headless tests --- .../portal_testing/PythonSeleniumTest/gui_test.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/gui_test.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/gui_test.py index b1e2400aa..10c42a970 100755 --- a/tools/developer_tools/portal_testing/PythonSeleniumTest/gui_test.py +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/gui_test.py @@ -41,10 +41,12 @@ def setUp(self): headless = os.getenv("CDB_TEST_HEADLESS", 'False').lower() in ('true', '1', 't', 'y', 'yes') if headless: opts.add_argument("headless") + opts.add_argument("window-size=1920,1080") self.driver = webdriver.Chrome(options=opts) self.driver.set_window_position(0, 0) - self.driver.maximize_window() + if not headless: + self.driver.maximize_window() self.driver.get(self.PORTAL_URL) self.portal = Portal(self.driver) @@ -64,8 +66,7 @@ def setUp(self): self.portal.login() def tearDown(self): - self.portal.logout() - self.driver.close() + self.driver.quit() def prepare_download_area(self): if os.path.exists(self.DATA_PATH): From bec5a0398703bfbddf7ca0c48a531eac083fcde7 Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Tue, 27 Oct 2026 11:56:50 -0500 Subject: [PATCH 13/34] Refactor the tests into modules by domain --- sbin/cdb_test.sh | 2 +- ...INVENTORY_MACHINE_ASSIGNMENT_ACCEPTANCE.md | 2 +- .../PythonSeleniumTest/gui_test.py | 240 ------------------ .../PythonSeleniumTest/tests/__init__.py | 0 .../PythonSeleniumTest/tests/base.py | 54 ++++ .../tests/test_browse_by.py | 28 ++ .../tests/test_cable_catalog.py | 32 +++ .../tests/test_cable_design.py | 32 +++ .../tests/test_cable_inventory.py | 28 ++ .../PythonSeleniumTest/tests/test_catalog.py | 40 +++ .../tests/test_connector_type.py | 24 ++ .../tests/test_inventory.py | 31 +++ .../PythonSeleniumTest/tests/test_location.py | 20 ++ .../tests/test_machine_design.py | 41 +++ .../tests/test_property_type.py | 20 ++ .../PythonSeleniumTest/tests/test_source.py | 28 ++ .../tests/test_user_info.py | 24 ++ 17 files changed, 404 insertions(+), 242 deletions(-) delete mode 100755 tools/developer_tools/portal_testing/PythonSeleniumTest/gui_test.py create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/tests/__init__.py create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/tests/base.py create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_browse_by.py create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_cable_catalog.py create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_cable_design.py create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_cable_inventory.py create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_catalog.py create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_connector_type.py create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_inventory.py create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_location.py create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_machine_design.py create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_property_type.py create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_source.py create mode 100644 tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_user_info.py diff --git a/sbin/cdb_test.sh b/sbin/cdb_test.sh index ba00a2478..23460f38f 100755 --- a/sbin/cdb_test.sh +++ b/sbin/cdb_test.sh @@ -87,7 +87,7 @@ fi if [[ $skiptest == 0 ]]; then export PATH=`pwd`:$PATH cd .. - pytest gui_test.py + pytest tests/ else echo "Error finding required chromedriver" fi diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/docs/INVENTORY_MACHINE_ASSIGNMENT_ACCEPTANCE.md b/tools/developer_tools/portal_testing/PythonSeleniumTest/docs/INVENTORY_MACHINE_ASSIGNMENT_ACCEPTANCE.md index 2dd544e28..a13f5d04a 100644 --- a/tools/developer_tools/portal_testing/PythonSeleniumTest/docs/INVENTORY_MACHINE_ASSIGNMENT_ACCEPTANCE.md +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/docs/INVENTORY_MACHINE_ASSIGNMENT_ACCEPTANCE.md @@ -1,6 +1,6 @@ # Inventory Machine Assignment Acceptance Coverage -Automated by `CdbPortalFunctionalTestSuite.test_inventory_machine_assignment` using the test database fixtures. +Automated by `tests/test_inventory.py::InventoryTest::test_inventory_machine_assignment` using the test database fixtures. ## Automated scenarios diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/gui_test.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/gui_test.py deleted file mode 100755 index 10c42a970..000000000 --- a/tools/developer_tools/portal_testing/PythonSeleniumTest/gui_test.py +++ /dev/null @@ -1,240 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) UChicago Argonne, LLC. All rights reserved. -See LICENSE file. -""" -import os -import unittest - -from selenium import webdriver - -from cdbSeleniumModules.browse_by import BrowseBy -from cdbSeleniumModules.cable_catalog import CableCatalog -from cdbSeleniumModules.cable_design import CableDesign -from cdbSeleniumModules.cable_inventory import CableInventory -from cdbSeleniumModules.catalog import Catalog -from cdbSeleniumModules.connector_type import ConnectorType -from cdbSeleniumModules.inventory import Inventory -from cdbSeleniumModules.location import Location -from cdbSeleniumModules.machineDesign import MachineDesign -from cdbSeleniumModules.portal import Portal -from cdbSeleniumModules.propertyType import PropertyType -from cdbSeleniumModules.source import Source -from cdbSeleniumModules.user_info import UserInfo - - -class CdbPortalFunctionalTestSuite(unittest.TestCase): - - PORTAL_URL = 'http://localhost:8080/cdb' - MACHINE_DESIGN_CSV_PATH = 'data/md-test.csv' - MACHINE_DESIGN_PROJECT = 'APS-U' - - DATA_PATH = "%s/data-dw" % os.getcwd() - - def setUp(self): - self.prepare_download_area() - opts = webdriver.ChromeOptions() - prefs = {"download.default_directory": self.DATA_PATH} - opts.add_experimental_option("prefs", prefs) - - headless = os.getenv("CDB_TEST_HEADLESS", 'False').lower() in ('true', '1', 't', 'y', 'yes') - if headless: - opts.add_argument("headless") - opts.add_argument("window-size=1920,1080") - - self.driver = webdriver.Chrome(options=opts) - self.driver.set_window_position(0, 0) - if not headless: - self.driver.maximize_window() - self.driver.get(self.PORTAL_URL) - - self.portal = Portal(self.driver) - self.property_type = PropertyType(self.driver) - self.catalog = Catalog(self.driver) - self.inventory = Inventory(self.driver) - self.cable_inventory = CableInventory(self.driver) - self.cable_catalog = CableCatalog(self.driver) - self.cable_design = CableDesign(self.driver) - self.machine_design = MachineDesign(self.driver) - self.location = Location(self.driver) - self.browse_by = BrowseBy(self.driver) - self.source = Source(self.driver) - self.user_info = UserInfo(self.driver) - self.connector_type = ConnectorType(self.driver) - - self.portal.login() - - def tearDown(self): - self.driver.quit() - - def prepare_download_area(self): - if os.path.exists(self.DATA_PATH): - # Clean up any residual downloads - for file in os.scandir(self.DATA_PATH): - os.remove(file.path) - else: - # Create directory - os.mkdir(self.DATA_PATH) - - def verify_file_downloaded(self, file_name): - file_path = "%s/%s" % (self.DATA_PATH, file_name) - exits = os.path.exists(file_path) - self.assertEqual(True, exits, msg='Downloaded file %s was not found' % file_name) - - def test_create_property_type(self): - self.property_type.add_sample_test_property_type() - self.property_type.delete_sample_test_property_type() - - def test_create_catalog_item(self): - self.catalog.start_create_sample_catalog_item() - self.catalog.finish_create_sample_catalog_item() - - self.catalog.display_more_columns() - self.catalog.search_for_sample_catalog_item() - self.catalog.add_log_to_catalog_item() - self.catalog.add_property_to_catalog_item(self) - self.catalog.add_image_to_catalog_item() - - self.catalog.delete_current_item() - - def test_machine_design(self): - self.machine_design.navigate_to_machine_design() - self.machine_design.input_hierarchy_from_sample_file(csvFile=self.MACHINE_DESIGN_CSV_PATH, - project=self.MACHINE_DESIGN_PROJECT) - - def test_machine_design_export(self): - self.machine_design.navigate_to_machine_design() - self.machine_design.filter_machine() - self.machine_design.export_machine(self) - - def test_machine_design_import(self): - self.machine_design.navigate_to_machine_design() - self.machine_design.import_machine(self) - - def test_machine_design_details_page(self): - self.machine_design.navigate_to_machine_design() - self.machine_design.test_detail_page(self) - - def test_machine_pages(self): - self.machine_design.navigate_to_machine_design() - self.machine_design.test_machine_pages() - - def test_browse_by_function_pages(self): - self.browse_by.navigate_to_browse_by_function() - self.browse_by.test_browse_by_function(self) - - def test_browse_by_owner(self): - self.browse_by.navigate_to_browse_by_owner() - self.browse_by.test_browse_by_owner(self) - - def test_browse_by_location(self): - self.browse_by.navigate_to_browse_by_location() - self.browse_by.test_browse_by_location(self) - - def test_catalog_pages(self): - self.catalog.navigate_to_catalog_list() - self.catalog.test_catalog_pages() - - def test_catalog_export(self): - self.catalog.navigate_to_catalog_list() - self.catalog.export_catalog(self) - - def test_catalog_import(self): - self.catalog.navigate_to_catalog_list() - self.catalog.test_import_create(self) - - def test_cable_catalog_pages(self): - self.cable_catalog.navigate_to_cable_catalog_list() - self.cable_catalog.test_cable_catalog_pages() - - def test_cable_catalog_detail_page(self): - self.cable_catalog.navigate_to_cable_catalog_list() - self.cable_catalog.test_cable_catalog_detail_page(self) - - def test_cable_catalog_export(self): - self.cable_catalog.navigate_to_cable_catalog_list() - self.cable_catalog.export_cable_catalog(self) - - def test_cable_catalog_import(self): - self.cable_catalog.navigate_to_cable_catalog_list() - self.cable_catalog.test_import_create(self) - - def test_inventory_pages(self): - self.inventory.navigate_to_inventory_list() - self.inventory.test_inventory_pages() - - def test_inventory_detail_page(self): - self.inventory.navigate_to_inventory_list() - self.inventory.test_inventory_detail_page(self) - - def test_inventory_machine_assignment(self): - self.inventory.test_machine_assignment_acceptance(self) - - def test_inventory_import(self): - self.inventory.navigate_to_inventory_list() - self.inventory.test_import_create(self) - - def test_cable_inventory_pages(self): - self.cable_inventory.navigate_to_cable_inventory_list() - self.cable_inventory.test_cable_inventory_pages() - - def test_cable_inventory_detail_page(self): - self.cable_inventory.navigate_to_cable_inventory_list() - self.cable_inventory.test_cable_inventory_detail_page(self) - - def test_cable_inventory_import(self): - self.cable_inventory.navigate_to_cable_inventory_list() - self.cable_inventory.test_import_create(self) - - def test_cable_design_pages(self): - self.cable_design.navigate_to_cable_design_list() - self.cable_design.test_cable_design_pages() - - def test_cable_design_detail_page(self): - self.cable_design.navigate_to_cable_design_list() - self.cable_design.test_cable_design_detail_page(self) - - def test_cable_design_export(self): - self.cable_design.navigate_to_cable_design_list() - self.cable_design.export_cable_design(self) - - def test_cable_design_import(self): - self.cable_design.navigate_to_cable_design_list() - self.cable_design.test_import_create(self) - - def test_source_pages(self): - self.source.navigate_to_source_list() - self.source.test_source_pages() - - def test_source_export(self): - self.source.navigate_to_source_list() - self.source.export_source(self) - - def test_source_import_pages(self): - self.source.navigate_to_source_list() - self.source.test_import_create(self) - - def test_user_info_pages(self): - self.user_info.navigate_to_user_info_list() - self.user_info.test_user_info_pages() - - def test_user_info_export(self): - self.user_info.navigate_to_user_info_list() - self.user_info.export_user_info(self) - - def test_connector_type_pages(self): - self.connector_type.navigate_to_connector_type_list() - self.connector_type.test_connector_type_pages() - - def test_connector_type_export(self): - self.connector_type.navigate_to_connector_type_list() - self.connector_type.export_connector_type(self) - - def test_location_pages(self): - self.location.navigate_to_location_list() - self.location.test_location_pages() - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/__init__.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/base.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/base.py new file mode 100644 index 000000000..6afad9328 --- /dev/null +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/base.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +""" +Copyright (c) UChicago Argonne, LLC. All rights reserved. +See LICENSE file. +""" +import os +import unittest + +from selenium import webdriver + +from cdbSeleniumModules.portal import Portal + + +class CdbPortalTestCase(unittest.TestCase): + + PORTAL_URL = 'http://localhost:8080/cdb' + DATA_PATH = "%s/data-dw" % os.getcwd() + + def setUp(self): + self.prepare_download_area() + opts = webdriver.ChromeOptions() + prefs = {"download.default_directory": self.DATA_PATH} + opts.add_experimental_option("prefs", prefs) + + headless = os.getenv("CDB_TEST_HEADLESS", 'False').lower() in ('true', '1', 't', 'y', 'yes') + if headless: + opts.add_argument("headless") + + self.driver = webdriver.Chrome(options=opts) + self.driver.set_window_position(0, 0) + self.driver.maximize_window() + self.driver.get(self.PORTAL_URL) + + self.portal = Portal(self.driver) + self.portal.login() + + def tearDown(self): + self.portal.logout() + self.driver.close() + + def prepare_download_area(self): + if os.path.exists(self.DATA_PATH): + # Clean up any residual downloads + for file in os.scandir(self.DATA_PATH): + os.remove(file.path) + else: + # Create directory + os.mkdir(self.DATA_PATH) + + def verify_file_downloaded(self, file_name): + file_path = "%s/%s" % (self.DATA_PATH, file_name) + exists = os.path.exists(file_path) + self.assertEqual(True, exists, msg='Downloaded file %s was not found' % file_name) diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_browse_by.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_browse_by.py new file mode 100644 index 000000000..384effb5f --- /dev/null +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_browse_by.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +""" +Copyright (c) UChicago Argonne, LLC. All rights reserved. +See LICENSE file. +""" + +from cdbSeleniumModules.browse_by import BrowseBy +from tests.base import CdbPortalTestCase + + +class BrowseByTest(CdbPortalTestCase): + + def setUp(self): + super().setUp() + self.browse_by = BrowseBy(self.driver) + + def test_browse_by_function_pages(self): + self.browse_by.navigate_to_browse_by_function() + self.browse_by.test_browse_by_function(self) + + def test_browse_by_owner(self): + self.browse_by.navigate_to_browse_by_owner() + self.browse_by.test_browse_by_owner(self) + + def test_browse_by_location(self): + self.browse_by.navigate_to_browse_by_location() + self.browse_by.test_browse_by_location(self) diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_cable_catalog.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_cable_catalog.py new file mode 100644 index 000000000..e9ac540bc --- /dev/null +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_cable_catalog.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +""" +Copyright (c) UChicago Argonne, LLC. All rights reserved. +See LICENSE file. +""" + +from cdbSeleniumModules.cable_catalog import CableCatalog +from tests.base import CdbPortalTestCase + + +class CableCatalogTest(CdbPortalTestCase): + + def setUp(self): + super().setUp() + self.cable_catalog = CableCatalog(self.driver) + + def test_cable_catalog_pages(self): + self.cable_catalog.navigate_to_cable_catalog_list() + self.cable_catalog.test_cable_catalog_pages() + + def test_cable_catalog_detail_page(self): + self.cable_catalog.navigate_to_cable_catalog_list() + self.cable_catalog.test_cable_catalog_detail_page(self) + + def test_cable_catalog_export(self): + self.cable_catalog.navigate_to_cable_catalog_list() + self.cable_catalog.export_cable_catalog(self) + + def test_cable_catalog_import(self): + self.cable_catalog.navigate_to_cable_catalog_list() + self.cable_catalog.test_import_create(self) diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_cable_design.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_cable_design.py new file mode 100644 index 000000000..636ad4548 --- /dev/null +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_cable_design.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +""" +Copyright (c) UChicago Argonne, LLC. All rights reserved. +See LICENSE file. +""" + +from cdbSeleniumModules.cable_design import CableDesign +from tests.base import CdbPortalTestCase + + +class CableDesignTest(CdbPortalTestCase): + + def setUp(self): + super().setUp() + self.cable_design = CableDesign(self.driver) + + def test_cable_design_pages(self): + self.cable_design.navigate_to_cable_design_list() + self.cable_design.test_cable_design_pages() + + def test_cable_design_detail_page(self): + self.cable_design.navigate_to_cable_design_list() + self.cable_design.test_cable_design_detail_page(self) + + def test_cable_design_export(self): + self.cable_design.navigate_to_cable_design_list() + self.cable_design.export_cable_design(self) + + def test_cable_design_import(self): + self.cable_design.navigate_to_cable_design_list() + self.cable_design.test_import_create(self) diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_cable_inventory.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_cable_inventory.py new file mode 100644 index 000000000..566d6d588 --- /dev/null +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_cable_inventory.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +""" +Copyright (c) UChicago Argonne, LLC. All rights reserved. +See LICENSE file. +""" + +from cdbSeleniumModules.cable_inventory import CableInventory +from tests.base import CdbPortalTestCase + + +class CableInventoryTest(CdbPortalTestCase): + + def setUp(self): + super().setUp() + self.cable_inventory = CableInventory(self.driver) + + def test_cable_inventory_pages(self): + self.cable_inventory.navigate_to_cable_inventory_list() + self.cable_inventory.test_cable_inventory_pages() + + def test_cable_inventory_detail_page(self): + self.cable_inventory.navigate_to_cable_inventory_list() + self.cable_inventory.test_cable_inventory_detail_page(self) + + def test_cable_inventory_import(self): + self.cable_inventory.navigate_to_cable_inventory_list() + self.cable_inventory.test_import_create(self) diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_catalog.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_catalog.py new file mode 100644 index 000000000..bfeec3459 --- /dev/null +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_catalog.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +""" +Copyright (c) UChicago Argonne, LLC. All rights reserved. +See LICENSE file. +""" + +from cdbSeleniumModules.catalog import Catalog +from tests.base import CdbPortalTestCase + + +class CatalogTest(CdbPortalTestCase): + + def setUp(self): + super().setUp() + self.catalog = Catalog(self.driver) + + def test_create_catalog_item(self): + self.catalog.start_create_sample_catalog_item() + self.catalog.finish_create_sample_catalog_item() + + self.catalog.display_more_columns() + self.catalog.search_for_sample_catalog_item() + self.catalog.add_log_to_catalog_item() + self.catalog.add_property_to_catalog_item(self) + self.catalog.add_image_to_catalog_item() + + self.catalog.delete_current_item() + + def test_catalog_pages(self): + self.catalog.navigate_to_catalog_list() + self.catalog.test_catalog_pages() + + def test_catalog_export(self): + self.catalog.navigate_to_catalog_list() + self.catalog.export_catalog(self) + + def test_catalog_import(self): + self.catalog.navigate_to_catalog_list() + self.catalog.test_import_create(self) diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_connector_type.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_connector_type.py new file mode 100644 index 000000000..5c0a78035 --- /dev/null +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_connector_type.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +""" +Copyright (c) UChicago Argonne, LLC. All rights reserved. +See LICENSE file. +""" + +from cdbSeleniumModules.connector_type import ConnectorType +from tests.base import CdbPortalTestCase + + +class ConnectorTypeTest(CdbPortalTestCase): + + def setUp(self): + super().setUp() + self.connector_type = ConnectorType(self.driver) + + def test_connector_type_pages(self): + self.connector_type.navigate_to_connector_type_list() + self.connector_type.test_connector_type_pages() + + def test_connector_type_export(self): + self.connector_type.navigate_to_connector_type_list() + self.connector_type.export_connector_type(self) diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_inventory.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_inventory.py new file mode 100644 index 000000000..c7085578d --- /dev/null +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_inventory.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +""" +Copyright (c) UChicago Argonne, LLC. All rights reserved. +See LICENSE file. +""" + +from cdbSeleniumModules.inventory import Inventory +from tests.base import CdbPortalTestCase + + +class InventoryTest(CdbPortalTestCase): + + def setUp(self): + super().setUp() + self.inventory = Inventory(self.driver) + + def test_inventory_pages(self): + self.inventory.navigate_to_inventory_list() + self.inventory.test_inventory_pages() + + def test_inventory_detail_page(self): + self.inventory.navigate_to_inventory_list() + self.inventory.test_inventory_detail_page(self) + + def test_inventory_machine_assignment(self): + self.inventory.test_machine_assignment_acceptance(self) + + def test_inventory_import(self): + self.inventory.navigate_to_inventory_list() + self.inventory.test_import_create(self) diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_location.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_location.py new file mode 100644 index 000000000..774d809ba --- /dev/null +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_location.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +""" +Copyright (c) UChicago Argonne, LLC. All rights reserved. +See LICENSE file. +""" + +from cdbSeleniumModules.location import Location +from tests.base import CdbPortalTestCase + + +class LocationTest(CdbPortalTestCase): + + def setUp(self): + super().setUp() + self.location = Location(self.driver) + + def test_location_pages(self): + self.location.navigate_to_location_list() + self.location.test_location_pages() diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_machine_design.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_machine_design.py new file mode 100644 index 000000000..9bb97b099 --- /dev/null +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_machine_design.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +""" +Copyright (c) UChicago Argonne, LLC. All rights reserved. +See LICENSE file. +""" + +from cdbSeleniumModules.machineDesign import MachineDesign +from tests.base import CdbPortalTestCase + + +class MachineDesignTest(CdbPortalTestCase): + + MACHINE_DESIGN_CSV_PATH = 'data/md-test.csv' + MACHINE_DESIGN_PROJECT = 'APS-U' + + def setUp(self): + super().setUp() + self.machine_design = MachineDesign(self.driver) + + def test_machine_design(self): + self.machine_design.navigate_to_machine_design() + self.machine_design.input_hierarchy_from_sample_file(csvFile=self.MACHINE_DESIGN_CSV_PATH, + project=self.MACHINE_DESIGN_PROJECT) + + def test_machine_design_export(self): + self.machine_design.navigate_to_machine_design() + self.machine_design.filter_machine() + self.machine_design.export_machine(self) + + def test_machine_design_import(self): + self.machine_design.navigate_to_machine_design() + self.machine_design.import_machine(self) + + def test_machine_design_details_page(self): + self.machine_design.navigate_to_machine_design() + self.machine_design.test_detail_page(self) + + def test_machine_pages(self): + self.machine_design.navigate_to_machine_design() + self.machine_design.test_machine_pages() diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_property_type.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_property_type.py new file mode 100644 index 000000000..1cfdf5d3f --- /dev/null +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_property_type.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +""" +Copyright (c) UChicago Argonne, LLC. All rights reserved. +See LICENSE file. +""" + +from cdbSeleniumModules.propertyType import PropertyType +from tests.base import CdbPortalTestCase + + +class PropertyTypeTest(CdbPortalTestCase): + + def setUp(self): + super().setUp() + self.property_type = PropertyType(self.driver) + + def test_create_property_type(self): + self.property_type.add_sample_test_property_type() + self.property_type.delete_sample_test_property_type() diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_source.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_source.py new file mode 100644 index 000000000..f05bee4db --- /dev/null +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_source.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +""" +Copyright (c) UChicago Argonne, LLC. All rights reserved. +See LICENSE file. +""" + +from cdbSeleniumModules.source import Source +from tests.base import CdbPortalTestCase + + +class SourceTest(CdbPortalTestCase): + + def setUp(self): + super().setUp() + self.source = Source(self.driver) + + def test_source_pages(self): + self.source.navigate_to_source_list() + self.source.test_source_pages() + + def test_source_export(self): + self.source.navigate_to_source_list() + self.source.export_source(self) + + def test_source_import_pages(self): + self.source.navigate_to_source_list() + self.source.test_import_create(self) diff --git a/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_user_info.py b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_user_info.py new file mode 100644 index 000000000..8ad711ddf --- /dev/null +++ b/tools/developer_tools/portal_testing/PythonSeleniumTest/tests/test_user_info.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +""" +Copyright (c) UChicago Argonne, LLC. All rights reserved. +See LICENSE file. +""" + +from cdbSeleniumModules.user_info import UserInfo +from tests.base import CdbPortalTestCase + + +class UserInfoTest(CdbPortalTestCase): + + def setUp(self): + super().setUp() + self.user_info = UserInfo(self.driver) + + def test_user_info_pages(self): + self.user_info.navigate_to_user_info_list() + self.user_info.test_user_info_pages() + + def test_user_info_export(self): + self.user_info.navigate_to_user_info_list() + self.user_info.export_user_info(self) From a1eefeed16ad8d38a0a4fddf74483ad6e3cd6f8e Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Tue, 27 Oct 2026 13:17:03 -0500 Subject: [PATCH 14/34] Display Assigned item column in the selection of housing parent from inventory --- .../itemDomainInventoryMachineAssignmentTree.xhtml | 1 + .../private/itemDomainMachineDesignTreeSelection.xhtml | 8 ++++++++ .../itemDomainMachineDesignListObjectTemplate.xhtml | 8 ++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentTree.xhtml b/src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentTree.xhtml index 968e59160..44c252a6b 100644 --- a/src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentTree.xhtml +++ b/src/java/CdbWebPortal/web/views/itemDomainInventory/private/itemDomainInventoryMachineAssignmentTree.xhtml @@ -12,6 +12,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 index af9d1ad18..f28e2f6b4 100644 --- a/src/java/CdbWebPortal/web/views/itemDomainMachineDesignBase/private/itemDomainMachineDesignTreeSelection.xhtml +++ b/src/java/CdbWebPortal/web/views/itemDomainMachineDesignBase/private/itemDomainMachineDesignTreeSelection.xhtml @@ -8,6 +8,9 @@ See LICENSE file. xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui"> + + @@ -22,6 +25,11 @@ See LICENSE file. + + + + + 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}"/> From 58abbb6692686d53927d7c7d83883e5e6274500d Mon Sep 17 00:00:00 2001 From: Dariusz Jarosz Date: Tue, 27 Oct 2026 13:17:26 -0500 Subject: [PATCH 15/34] Enable filtering of housing column from catalog details page. --- .../private/locatableItemListLocationObject.xhtml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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">