From 615af6ac6b206605e229b48aacd8eb561e2c46ef Mon Sep 17 00:00:00 2001 From: Vagisha Sharma Date: Fri, 10 Jul 2026 23:23:22 -0700 Subject: [PATCH] Limit the Isotope/Structural modification grids to the current folder tree --- .../panoramapublic/PanoramaPublicSchema.java | 35 +++++++++ .../query/modification/ModificationsView.java | 12 +++- .../PanoramaPublicModificationsTest.java | 72 +++++++++++++++++++ 3 files changed, 117 insertions(+), 2 deletions(-) diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicSchema.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicSchema.java index c40c0826..124fcf9c 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicSchema.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicSchema.java @@ -92,6 +92,11 @@ public class PanoramaPublicSchema extends UserSchema public static final String TABLE_EXPT_ISOTOPE_MOD_INFO = "ExperimentIsotopeModInfo"; public static final String TABLE_ISOTOPE_UNIMOD_INFO = "IsotopeUnimodInfo"; + // Queries behind the Structural and Isotope modification web parts. Both aggregate over + // container-scoped targetedms tables, so they must stay scoped to the current folder tree. + public static final String QUERY_STRUCTURAL_MODIFICATIONS = "StructuralModifications"; + public static final String QUERY_ISOTOPE_MODIFICATIONS = "IsotopeModifications"; + public static final String TABLE_LIB_DEPENDENCY_TYPE = "SpecLibDependencyType"; public static final String TABLE_LIB_SOURCE_TYPE = "SpecLibSourceType"; @@ -430,9 +435,39 @@ protected void populateButtonBar(DataView view, ButtonBar bar) }; } + else if (QUERY_ISOTOPE_MODIFICATIONS.equalsIgnoreCase(settings.getQueryName()) + || QUERY_STRUCTURAL_MODIFICATIONS.equalsIgnoreCase(settings.getQueryName())) + { + // The modification web part title links to this query grid. Restrict it to the current folder tree. + // The standalone web part grid is restricted the same way in ModificationsView. + QueryView view = new QueryView(this, settings, errors) + { + @Override + protected ContainerFilter getContainerFilter() + { + return limitContainerScope(super.getContainerFilter(), getContainer(), getUser()); + } + }; + view.setAllowableContainerFilterTypes(ContainerFilter.Type.Current, ContainerFilter.Type.CurrentAndSubfolders); + return view; + } + return super.createView(context, settings, errors); } + // Limit the modification queries to the current folder tree. setAllowableContainerFilterTypes only applies + // to the options in the Folder Filter menu. query.containerFilterName=AllFolders in the URL still gets applied. + public static ContainerFilter limitContainerScope(ContainerFilter cf, Container container, User user) + { + if (cf != null + && cf.getType() != ContainerFilter.Type.Current + && cf.getType() != ContainerFilter.Type.CurrentAndSubfolders) + { + return ContainerFilter.Type.CurrentAndSubfolders.create(container, user); + } + return cf; + } + @Override public Set getTableNames() { diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/modification/ModificationsView.java b/panoramapublic/src/org/labkey/panoramapublic/query/modification/ModificationsView.java index b2165bf1..3edfd2bb 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/query/modification/ModificationsView.java +++ b/panoramapublic/src/org/labkey/panoramapublic/query/modification/ModificationsView.java @@ -55,6 +55,14 @@ protected QuerySettings createQuerySettings(ViewContext portalCtx) return settings; } + @Override + protected ContainerFilter getContainerFilter() + { + // The standalone web part grid reads the container scope from the URL, which could be set to "AllFolders". + // Restrict it the same way createView does. + return PanoramaPublicSchema.limitContainerScope(super.getContainerFilter(), getContainer(), getUser()); + } + public static class StructuralModsView extends ModificationsView { public StructuralModsView(ViewContext portalCtx) @@ -64,7 +72,7 @@ public StructuralModsView(ViewContext portalCtx) public StructuralModsView(ViewContext portalCtx, @Nullable ExperimentAnnotations exptAnnotations) { - super(portalCtx, "StructuralModifications", "Structural Modifications", "ExperimentStructuralModInfo", exptAnnotations); + super(portalCtx, PanoramaPublicSchema.QUERY_STRUCTURAL_MODIFICATIONS, "Structural Modifications", "ExperimentStructuralModInfo", exptAnnotations); } } @@ -77,7 +85,7 @@ public IsotopeModsView(ViewContext portalCtx) public IsotopeModsView(ViewContext portalCtx, @Nullable ExperimentAnnotations exptAnnotations) { - super(portalCtx, "IsotopeModifications", "Isotope Modifications", "ExperimentIsotopeModInfo", exptAnnotations); + super(portalCtx, PanoramaPublicSchema.QUERY_ISOTOPE_MODIFICATIONS, "Isotope Modifications", "ExperimentIsotopeModInfo", exptAnnotations); } } } diff --git a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PanoramaPublicModificationsTest.java b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PanoramaPublicModificationsTest.java index 8ccd12c0..89ebf6cc 100644 --- a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PanoramaPublicModificationsTest.java +++ b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PanoramaPublicModificationsTest.java @@ -5,18 +5,25 @@ import org.labkey.api.util.Pair; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.Locator; +import org.labkey.test.WebTestHelper; import org.labkey.test.categories.External; import org.labkey.test.categories.MacCossLabModules; import org.labkey.test.components.WebPart; +import org.labkey.test.components.html.BootstrapMenu; import org.labkey.test.components.panoramapublic.TargetedMsExperimentWebPart; import org.labkey.test.pages.panoramapublic.DataValidationPage; import org.labkey.test.util.DataRegionTable; +import org.labkey.test.util.EscapeUtil; import org.labkey.test.util.Ext4Helper; import org.labkey.test.util.TextSearcher; +import org.openqa.selenium.WebElement; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -92,6 +99,12 @@ public void testAddModInfo() testCustomizeGrid(STRUCTURAL_MOD, true); testCustomizeGrid(ISOTOPE_MOD, true); + // The web part title links to the full query grid. It must not let a caller widen the + // scope to all folders. + verifyModificationQueryScopeIsRestricted(); + // The standalone web part grid on this page must not be widened by a URL either. + verifyModificationWebPartScopeIsRestricted(); + // Match structural modification goToExperimentDetailsPage(); testSaveMatchForStructuralMod(propionylation, List.of(propionyl, new Unimod(206, "Delta:H(4)C(3)O(1)", propionyl.getFormula())), @@ -113,6 +126,65 @@ public void testAddModInfo() testCopy(projectName, folderName, experimentTitle, folderName + " Copy"); } + // CreateView on these queries restricts the grids to the current folder or the current folder and subfolders. + // Check that a wider scope asked for in the URL is restricted, and that an allowed scope still works. + private void verifyModificationQueryScopeIsRestricted() + { + for (String queryName : List.of("IsotopeModifications", "StructuralModifications")) + { + // The Folder Filter dropdown offers the narrow scopes but not All Folders. + assertFolderFilterMenuHidesAllFolders(queryName); + // A URL asking for All Folders is restricted to the current and subfolders. + assertContainerFilterScope(queryName, "AllFolders", DataRegionTable.ContainerFilterType.CURRENT_AND_SUBFOLDERS); + // "Current and Subfolders" is allowed. + assertContainerFilterScope(queryName, "CurrentAndSubfolders", DataRegionTable.ContainerFilterType.CURRENT_AND_SUBFOLDERS); + } + } + + // The standalone modification web parts (added to a folder page, not the experiment details page) + // read the scope straight from the URL. Check that a wider folder scope in the URL is restricted there too. + private void verifyModificationWebPartScopeIsRestricted() + { + for (String webPartName : List.of(STRUCTURAL_MOD, ISOTOPE_MOD)) + { + // Go to the dashboard tab that holds the web parts, then re-request the same page with a + // "AllFolders" filter in the URL. The web part's data region is named after its title. + goToDashboard(); + String url = getCurrentRelativeURL(); + url += (url.contains("?") ? "&" : "?") + EscapeUtil.encode(webPartName) + ".containerFilterName=AllFolders"; + beginAt(url); + var grid = new DataRegionTable(webPartName, this); + assertEquals("Standalone '" + webPartName + "' web part should restrict an All Folders URL to the current and subfolders", + DataRegionTable.ContainerFilterType.CURRENT_AND_SUBFOLDERS, grid.getContainerFilter()); + } + } + + private void assertFolderFilterMenuHidesAllFolders(String queryName) + { + beginAt(WebTestHelper.buildURL("query", getCurrentContainerPath(), "executeQuery", + Map.of("schemaName", "panoramapublic", "query.queryName", queryName))); + BootstrapMenu viewsMenu = new DataRegionTable("query", this).getViewsMenu(); + viewsMenu.openMenuTo("Folder Filter", "Folder Filter"); + List options = viewsMenu.findVisibleMenuItems().stream().map(WebElement::getText).map(String::trim).toList(); + // The allowed scope is present. This also guards against an empty menu making the check below pass for free. + assertTrue("Folder Filter for query '" + queryName + "' should offer 'Current folder and subfolders'. Found: " + options, + options.contains(DataRegionTable.ContainerFilterType.CURRENT_AND_SUBFOLDERS.getLabel())); + // All Folders is not offered. + assertFalse("Folder Filter for query '" + queryName + "' must not offer 'All Folders'. Found: " + options, + options.contains(DataRegionTable.ContainerFilterType.ALL_FOLDERS.getLabel())); + } + + private void assertContainerFilterScope(String queryName, String requestedFilterName, DataRegionTable.ContainerFilterType expected) + { + beginAt(WebTestHelper.buildURL("query", getCurrentContainerPath(), "executeQuery", + Map.of("schemaName", "panoramapublic", + "query.queryName", queryName, + "query.containerFilterName", requestedFilterName))); + var grid = new DataRegionTable("query", this); + assertEquals("Folder Filter for query '" + queryName + "' requested as '" + requestedFilterName + "' was not the expected scope", + expected, grid.getContainerFilter()); + } + private void testNoMatchForStructuralMod(String modificationName) { var modsTable = new DataRegionTable(STRUCTURAL_MOD, this);