diff --git a/api/src/org/labkey/api/query/QueryView.java b/api/src/org/labkey/api/query/QueryView.java index be99e20907c..bdf1e5c638e 100644 --- a/api/src/org/labkey/api/query/QueryView.java +++ b/api/src/org/labkey/api/query/QueryView.java @@ -1050,52 +1050,6 @@ public ActionButton createDeleteButton(boolean showConfirmation) return null; } - public ActionButton createDeleteAllRowsButton(String tableNoun) - { - ActionButton deleteAllRows = new ActionButton("Delete All Rows"); - deleteAllRows.setDisplayPermission(AdminPermission.class); - deleteAllRows.setActionType(ActionButton.Action.SCRIPT); - deleteAllRows.setScript( - "LABKEY.requiresExt4Sandbox(function() {" + - "Ext4.Msg.confirm('Confirm Deletion', 'Are you sure you wish to delete all rows in this " + tableNoun + "? This action cannot be undone and will result in an empty " + tableNoun + ".', function(button){" + - "if (button == 'yes'){" + - "var waitMask = Ext4.Msg.wait('Deleting Rows...', 'Delete Rows'); " + - "Ext4.Ajax.request({ " + - "url : LABKEY.ActionURL.buildURL('query', 'truncateTable'), " + - "method : 'POST', " + - "success: function(response) " + - "{" + - "waitMask.close(); " + - "var data = Ext4.JSON.decode(response.responseText); " + - "Ext4.Msg.show({ " + - "title : 'Success', " + - "buttons : Ext4.MessageBox.OK, " + - "msg : data.deletedRows + ' rows deleted', " + - "fn: function(btn) " + - "{ " + - "if(btn == 'ok') " + - "{ " + - "window.location.reload(); " + - "} " + - "} " + - "})" + - "}, " + - "failure : function(response, opts) " + - "{ " + - "waitMask.close(); " + - "Ext4.getBody().unmask(); " + - "LABKEY.Utils.displayAjaxErrorResponse(response, opts); " + - "}, " + - "jsonData : {schemaName : " + PageFlowUtil.jsString(getQueryDef().getSchema().getName()) + ", queryName : " + PageFlowUtil.jsString(getQueryDef().getName()) + "}, " + - "scope : this " + - "});" + - "}" + - "});" + - "});" - ); - return deleteAllRows; - } - public ActionButton createInsertMenuButton() { return createInsertMenuButton(null, null); diff --git a/list/src/org/labkey/list/controllers/ListController.java b/list/src/org/labkey/list/controllers/ListController.java index 44cda4aa4d5..c6dfd777348 100644 --- a/list/src/org/labkey/list/controllers/ListController.java +++ b/list/src/org/labkey/list/controllers/ListController.java @@ -425,6 +425,97 @@ public List> getListContainerMap() } } + @RequiresPermission(AdminPermission.class) + public static class TruncateListDataAction extends ConfirmAction + { + private boolean canTruncate(Container listContainer, int listId) + { + ListDef listDef = ListManager.get().getList(listContainer, listId); + ListDefinitionImpl list = ListDefinitionImpl.of(listDef); + + if (list == null || !list.getAllowDelete()) + return false; + + return list.getContainer().hasPermission(getUser(), AdminPermission.class); + } + + @Override + public String getConfirmText() + { + return "Confirm Delete All Data"; + } + + @Override + public void validateCommand(ListDeletionForm form, Errors errors) + { + Container currentContainer = getContainer(); + List errorMessages = new ArrayList<>(); + Collection listIDs; + if (form.getListIds() != null) + listIDs = form.getListIds(); + else + listIDs = DataRegionSelection.getSelected(form.getViewContext(), true); + + for (Pair pair : getListIdContainerPairs(listIDs, currentContainer, errorMessages)) + { + var listId = pair.first; + var listContainer = pair.second; + + if (canTruncate(listContainer, listId)) + { + form.getListContainerMap().add(pair); + } + else + errorMessages.add(String.format("You do not have permission to delete data for list %s in container %s", listId, listContainer.getName())); + } + + if (!errorMessages.isEmpty()) + errors.reject(ERROR_MSG, StringUtils.join(errorMessages, "\n")); + + if (form.getListContainerMap().isEmpty()) + errors.reject(ERROR_MSG, "You must specify a list or lists to delete data from."); + } + + @Override + public ModelAndView getConfirmView(ListDeletionForm form, BindException errors) + { + if (getPageConfig().getTitle() == null) + setTitle("Confirm Delete All Data"); + return new JspView<>("/org/labkey/list/view/truncateListData.jsp", form, errors); + } + + @Override + public boolean handlePost(ListDeletionForm form, BindException errors) + { + Container containerDataToDelete = getContainer(); + for (Pair pair : form.getListContainerMap()) + { + Container listDefContainer = pair.second; + ListDefinition listDef = ListService.get().getList(listDefContainer, pair.first); + if (null != listDef) + { + try + { + TableInfo table = listDef.getTable(getUser(), listDefContainer); + if (table != null && table.getUpdateService() != null) + table.getUpdateService().truncateRows(getUser(), containerDataToDelete, null, null); + } + catch (Exception e) + { + errors.reject(ERROR_MSG, "Error deleting data from list '" + listDef.getName() + "': " + e.getMessage()); + } + } + } + + return !errors.hasErrors(); + } + + @Override @NotNull + public URLHelper getSuccessURL(ListDeletionForm form) + { + return form.getReturnUrlHelper(getBeginURL(getContainer())); + } + } @RequiresPermission(ReadPermission.class) public class GridAction extends SimpleViewAction @@ -958,10 +1049,11 @@ public Pair getAttachment(ListAttachmentForm form) if (listDef == null) throw new NotFoundException("List does not exist in this container"); - if (!listDef.hasListItemForEntityId(form.getEntityId(), getUser())) + Container dataContainer = listDef.getListItemContainerForDownload(form.getEntityId(), getUser(), ReadPermission.class); + if (dataContainer == null) throw new NotFoundException("List does not have an item for the entityid"); - AttachmentParent parent = new ListItemAttachmentParent(form.getEntityId(), getContainer()); + AttachmentParent parent = new ListItemAttachmentParent(form.getEntityId(), dataContainer); return new Pair<>(parent, form.getName()); } diff --git a/list/src/org/labkey/list/model/ListDefinitionImpl.java b/list/src/org/labkey/list/model/ListDefinitionImpl.java index b63b108c2e9..72ec6dbcc66 100644 --- a/list/src/org/labkey/list/model/ListDefinitionImpl.java +++ b/list/src/org/labkey/list/model/ListDefinitionImpl.java @@ -54,6 +54,7 @@ import org.labkey.api.reader.DataLoader; import org.labkey.api.reader.MapLoader; import org.labkey.api.security.User; +import org.labkey.api.security.permissions.Permission; import org.labkey.api.util.ReentrantLockWithName; import org.labkey.api.util.URLHelper; import org.labkey.api.view.ActionURL; @@ -521,19 +522,36 @@ private ListItem getListItem(SimpleFilter filter, User user, Container c) return impl; } - public boolean hasListItemForEntityId(String entityId, User user) + public Container getListItemContainerForDownload(String entityId, User user, Class permissionClass) { - return hasListItem(new SimpleFilter(FieldKey.fromParts("EntityId"), entityId), user, getContainer()); - } - - private boolean hasListItem(SimpleFilter filter, User user, Container c) - { - TableInfo tbl = getTable(user, c); + Container c = getContainer(); + SimpleFilter filter = new SimpleFilter(FieldKey.fromParts("EntityId"), entityId); + // Use a relax CF to find the list items, permission will be validated later + ContainerFilter cf = ContainerFilter.Type.AllInProjectPlusShared.create(c, user); + TableInfo tbl = getTable(user, c, cf); if (null == tbl) - return false; + return null; + + Map row = null; + + try + { + row = new TableSelector(tbl, filter, null).getMap(); + } + catch (IllegalStateException e) + { + /* more than one row matches */ + } + + if (row == null) + return null; + + Container dataContainer = row.get("Container") != null ? ContainerManager.getForId(row.get("Container").toString()) : null; + if (dataContainer != null && dataContainer.hasPermission(user, permissionClass)) + return dataContainer; - return new TableSelector(tbl, filter, null).exists(); + return null; } @Override diff --git a/list/src/org/labkey/list/model/ListManagerSchema.java b/list/src/org/labkey/list/model/ListManagerSchema.java index ff16718f3d2..7b1854c022c 100644 --- a/list/src/org/labkey/list/model/ListManagerSchema.java +++ b/list/src/org/labkey/list/model/ListManagerSchema.java @@ -21,6 +21,8 @@ import org.labkey.api.data.ActionButton; import org.labkey.api.data.ButtonBar; import org.labkey.api.data.Container; +import org.labkey.api.data.DataRegion; +import org.labkey.api.data.MenuButton; import org.labkey.api.data.ContainerFilter; import org.labkey.api.data.ContainerManager; import org.labkey.api.data.DisplayColumn; @@ -38,6 +40,7 @@ import org.labkey.api.query.QueryView; import org.labkey.api.query.UserSchema; import org.labkey.api.security.User; +import org.labkey.api.security.permissions.AdminPermission; import org.labkey.api.util.ButtonBuilder; import org.labkey.api.util.LinkBuilder; import org.labkey.api.view.ActionURL; @@ -162,14 +165,37 @@ private ButtonBuilder.Button createImportListArchiveButton() @Override public ActionButton createDeleteButton() { + if (!getContainer().hasPermission(getUser(), DesignListPermission.class)) + return null; + + if (!getContainer().hasPermission(getUser(), AdminPermission.class)) + { + ActionURL urlDelete = new ActionURL(ListController.DeleteListDefinitionAction.class, getContainer()); + urlDelete.addReturnUrl(getReturnUrl()); + ActionButton btnDelete = new ActionButton(urlDelete, "Delete"); + btnDelete.setIconCls("trash"); + btnDelete.setActionType(ActionButton.Action.GET); + btnDelete.setRequiresSelection(true); + return btnDelete; + } + + MenuButton menuDelete = new MenuButton("Delete"); + menuDelete.setIconCls("trash"); + menuDelete.setDisplayPermission(DesignListPermission.class); + menuDelete.setRequiresSelection(true); + ActionURL urlDelete = new ActionURL(ListController.DeleteListDefinitionAction.class, getContainer()); urlDelete.addReturnUrl(getReturnUrl()); - ActionButton btnDelete = new ActionButton(urlDelete, "Delete"); - btnDelete.setIconCls("trash"); - btnDelete.setActionType(ActionButton.Action.GET); - btnDelete.setDisplayPermission(DesignListPermission.class); - btnDelete.setRequiresSelection(true); - return btnDelete; + menuDelete.addMenuItem("Delete List", "if (verifySelected(" + DataRegion.getJavaScriptObjectReference(getDataRegionName()) + ".form, \"" + + urlDelete.getLocalURIString() + "\", \"GET\", \"rows\")) " + DataRegion.getJavaScriptObjectReference(getDataRegionName()) + ".form.submit()"); + + + ActionURL urlTruncate = new ActionURL(ListController.TruncateListDataAction.class, getContainer()); + urlTruncate.addReturnUrl(getReturnUrl()); + menuDelete.addMenuItem("Delete All Data from List", "if (verifySelected(" + DataRegion.getJavaScriptObjectReference(getDataRegionName()) + ".form, \"" + + urlTruncate.getLocalURIString() + "\", \"GET\", \"rows\")) " + DataRegion.getJavaScriptObjectReference(getDataRegionName()) + ".form.submit()"); + + return menuDelete; } private ActionButton createExportArchiveButton() diff --git a/list/src/org/labkey/list/model/ListQueryUpdateService.java b/list/src/org/labkey/list/model/ListQueryUpdateService.java index 8110e0c2b13..936cf53af26 100644 --- a/list/src/org/labkey/list/model/ListQueryUpdateService.java +++ b/list/src/org/labkey/list/model/ListQueryUpdateService.java @@ -767,7 +767,7 @@ public void deleteRelatedListData(final User user, final Container container) ListManager.get().deleteIndexedList(_list); // Delete attachments and discussions associated with a list in batches of 1,000 - new TableSelector(getDbTable(), Collections.singleton("entityId")).forEachBatch(String.class, 1000, new ForEachBatchBlock<>() + new TableSelector(getDbTable(), Collections.singleton("entityId"), SimpleFilter.createContainerFilter(container), null).forEachBatch(String.class, 1000, new ForEachBatchBlock<>() { @Override public boolean accept(String entityId) diff --git a/list/src/org/labkey/list/view/ListQueryView.java b/list/src/org/labkey/list/view/ListQueryView.java index a8a79d81e3e..5e01aaa726d 100644 --- a/list/src/org/labkey/list/view/ListQueryView.java +++ b/list/src/org/labkey/list/view/ListQueryView.java @@ -78,8 +78,6 @@ protected void populateButtonBar(DataView view, ButtonBar bar) ActionButton btnUpload = new ActionButton("Design", designURL); bar.add(btnUpload); } - if (canDelete()) - bar.add(super.createDeleteAllRowsButton("list")); } public ListDefinition getList() diff --git a/list/src/org/labkey/list/view/truncateListData.jsp b/list/src/org/labkey/list/view/truncateListData.jsp new file mode 100644 index 00000000000..a7ef0694457 --- /dev/null +++ b/list/src/org/labkey/list/view/truncateListData.jsp @@ -0,0 +1,70 @@ +<% +/* + * Copyright (c) 2009-2016 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +%> +<%@ page import="org.labkey.api.data.Container" %> +<%@ page import="org.labkey.api.data.TableInfo" %> +<%@ page import="org.labkey.api.data.TableSelector" %> +<%@ page import="org.labkey.api.exp.list.ListDefinition" %> +<%@ page import="org.labkey.api.exp.list.ListService" %> +<%@ page import="org.labkey.list.controllers.ListController" %> +<%@ page import="java.util.ArrayList" %> +<%@ page import="java.util.HashMap" %> +<%@ page import="java.util.List" %> +<%@ page import="java.util.Map" %> +<%@ page import="java.util.Objects" %> +<%@ page import="org.labkey.list.model.ListQuerySchema" %> +<%@ page import="org.labkey.api.security.User" %> +<%@ page extends="org.labkey.api.jsp.FormPage" %> +<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib"%> +<% + ListController.ListDeletionForm form = (ListController.ListDeletionForm)getModelBean(); + Map> definitions = new HashMap<>(); + Container currentContainer = form.getContainer(); + String currentPath = currentContainer.getPath(); + User user = form.getUser(); + ListQuerySchema listQuerySchema = new ListQuerySchema(user, currentContainer); + + form.getListContainerMap() + .stream() + .map(pair -> ListService.get().getList(pair.second, pair.first)) + .filter(Objects::nonNull) + .forEach(listDef -> { + var container = listDef.getContainer(); + if (!definitions.containsKey(container)) + definitions.put(container, new ArrayList<>()); + definitions.get(container).add(listDef); + }); +%> + +

Are you sure you want to delete all data in <%= h(currentPath) %> from the following Lists? This action cannot be undone and will result in an empty list.

+ +<% for (var entry : definitions.entrySet()) { %> +
+ Defined in <%= h(entry.getKey().getPath()) %>: +
    + <% for (var listDef : entry.getValue()) { + TableInfo table = listQuerySchema.getTable(listDef.getName()); + long count = (table != null) ? new TableSelector(table).getRowCount() : 0; + %> +
  • + <%= simpleLink(listDef.getName(), listDef.urlFor(ListController.GridAction.class, listDef.getContainer())) %> + — <%= count %> row<%= h(count != 1 ? "s" : "") %> in <%= h(currentPath) %> +
  • + <% } %> +
+
+<% } %>