Browse Source

wxGUI/datacatalog: Allow delete on multiple mapsets in Data catalog (#795)

Add the function delete_mapsets_interactively() which is called after selecting more mapsets. Preserves the original
delete_mapset_interactively() function with the db/loc/mapset parameters as a wrapper of the new function. 

Checks ahead of time not to delete current or PERMANENT mapsets. Nothing is deleted if problem detected.
Lists all mapset with a problem (but not all problems for each mapset if more than one applies).

Return value indicates change/modification. Error on the first mapset is no change. Error in the middle
is a change, but operation is interrupted, i.e. some possibly deleted and some not.
Return value is geared towards updating (or not updating) displayed tree in data catalog and gives exact result,
so data catalog is reloaded only when one or more mapsets were (actually) deleted.
Message on error is more vague providing same info for all cases regardless of deleted mapsets
(assuming the reload of the tree and (in future better) checking ahead of time).

Exceptions/Errors are responsibility of the delete_mapsets_interactively() function.
General Exception try-except removed from the callers.

Co-authored-by: Vaclav Petras <wenzeslaus@gmail.com>
Linda Kladivova 4 years ago
parent
commit
0bf022093d
3 changed files with 135 additions and 70 deletions
  1. 33 19
      gui/wxpython/datacatalog/tree.py
  2. 3 9
      gui/wxpython/gis_set.py
  3. 99 42
      gui/wxpython/startup/guiutils.py

+ 33 - 19
gui/wxpython/datacatalog/tree.py

@@ -34,13 +34,15 @@ from gui_core.treeview import TreeView
 from gui_core.wrap import Menu
 from datacatalog.dialogs import CatalogReprojectionDialog
 from icons.icon import MetaIcon
-from startup.guiutils import (create_mapset_interactively,
-                              create_location_interactively,
-                              rename_mapset_interactively,
-                              rename_location_interactively,
-                              delete_mapset_interactively,
-                              delete_location_interactively,
-                              download_location_interactively)
+from startup.guiutils import (
+    create_mapset_interactively,
+    create_location_interactively,
+    rename_mapset_interactively,
+    rename_location_interactively,
+    delete_mapsets_interactively,
+    delete_location_interactively,
+    download_location_interactively,
+)
 
 from grass.pydispatch.signal import Signal
 
@@ -521,6 +523,8 @@ class DataCatalogTree(TreeView):
             self._popupMenuLocation()
         elif self.selected_grassdb[0] and not self.selected_location[0] and len(self.selected_grassdb) == 1:
             self._popupMenuGrassDb()
+        elif len(self.selected_mapset) > 1:
+            self._popupMenuMultipleMapsets()
         else:
             self._popupMenuEmpty()
 
@@ -955,19 +959,18 @@ class DataCatalogTree(TreeView):
 
     def OnDeleteMapset(self, event):
         """
-        Delete selected mapset
+        Delete selected mapset or mapsets
         """
-        try:
-            if (delete_mapset_interactively(
-                    self,
-                    self.selected_grassdb[0].data['name'],
-                    self.selected_location[0].data['name'],
-                    self.selected_mapset[0].data['name'])):
-                self.ReloadTreeItems()
-        except Exception as e:
-            GError(parent=self,
-                   message=_("Unable to delete mapset: %s") % e,
-                   showTraceback=False)
+        mapsets = []
+        for i in range(len(self.selected_mapset)):
+            # Append to the list of tuples
+            mapsets.append((
+                self.selected_grassdb[i].data['name'],
+                self.selected_location[i].data['name'],
+                self.selected_mapset[i].data['name']
+            ))
+        if delete_mapsets_interactively(self, mapsets):
+            self.ReloadTreeItems()
 
     def OnDeleteLocation(self, event):
         """
@@ -1349,6 +1352,17 @@ class DataCatalogTree(TreeView):
         self.PopupMenu(menu)
         menu.Destroy()
 
+    def _popupMenuMultipleMapsets(self):
+        """Create popup menu for multiple selected mapsets"""
+        menu = Menu()
+
+        item = wx.MenuItem(menu, wx.ID_ANY, _("&Delete mapsets"))
+        menu.AppendItem(item)
+        self.Bind(wx.EVT_MENU, self.OnDeleteMapset, item)
+
+        self.PopupMenu(menu)
+        menu.Destroy()
+
     def _popupMenuEmpty(self):
         """Create empty popup when multiple different types of items are selected"""
         menu = Menu()

+ 3 - 9
gui/wxpython/gis_set.py

@@ -595,15 +595,9 @@ class GRASSStartup(wx.Frame):
         """
         location = self.listOfLocations[self.lblocations.GetSelection()]
         mapset = self.listOfMapsets[self.lbmapsets.GetSelection()]
-        try:
-            if (delete_mapset_interactively(self, self.gisdbase,
-                                            location, mapset)):
-                self.OnSelectLocation(None)
-                self.lbmapsets.SetSelection(0)
-        except Exception as e:
-            GError(parent=self,
-                   message=_("Unable to delete mapset: %s") % e,
-                   showTraceback=False)
+        if (delete_mapset_interactively(self, self.gisdbase, location, mapset)):
+            self.OnSelectLocation(None)
+            self.lbmapsets.SetSelection(0)
 
     def OnDeleteLocation(self, event):
         """

+ 99 - 42
gui/wxpython/startup/guiutils.py

@@ -20,6 +20,7 @@ import sys
 import wx
 
 import grass.script as gs
+from grass.script import gisenv
 
 from core import globalvar
 from core.gcmd import GError, GMessage, DecodeString, RunCommand
@@ -205,7 +206,7 @@ def create_mapset_interactively(guiparent, grassdb, location):
             mapset = None
             GError(
                 parent=guiparent,
-                message=_("Unable to create new mapset: %s") % err,
+                message=_("Unable to create new mapset: {}").format(err),
                 showTraceback=False,
             )
     dlg.Destroy()
@@ -283,7 +284,7 @@ def rename_mapset_interactively(guiparent, grassdb, location, mapset):
     dlg = MapsetDialog(
         parent=guiparent,
         default=mapset,
-        message=_("Current name: %s\n\nEnter new name:") % mapset,
+        message=_("Current name: {}\n\nEnter new name:").format(mapset),
         caption=_("Rename selected mapset"),
         database=grassdb,
         location=location,
@@ -298,7 +299,7 @@ def rename_mapset_interactively(guiparent, grassdb, location, mapset):
             wx.MessageBox(
                 parent=guiparent,
                 caption=_("Error"),
-                message=_("Unable to rename mapset.\n\n%s") % err,
+                message=_("Unable to rename mapset.\n\n{}").format(err),
                 style=wx.OK | wx.ICON_ERROR | wx.CENTRE,
             )
     dlg.Destroy()
@@ -312,7 +313,7 @@ def rename_location_interactively(guiparent, grassdb, location):
     dlg = LocationDialog(
         parent=guiparent,
         default=location,
-        message=_("Current name: %s\n\nEnter new name:") % location,
+        message=_("Current name: {}\n\nEnter new name:").format(location),
         caption=_("Rename selected location"),
         database=grassdb,
     )
@@ -326,7 +327,7 @@ def rename_location_interactively(guiparent, grassdb, location):
             wx.MessageBox(
                 parent=guiparent,
                 caption=_("Error"),
-                message=_("Unable to rename location.\n\n%s") % err,
+                message=_("Unable to rename location.\n\n{}").format(err),
                 style=wx.OK | wx.ICON_ERROR | wx.CENTRE,
             )
     else:
@@ -359,46 +360,103 @@ def download_location_interactively(guiparent, grassdb):
 
 
 def delete_mapset_interactively(guiparent, grassdb, location, mapset):
+    """Delete one mapset with user interaction.
+
+    This is currently just a convenience wrapper for delete_mapsets_interactively().
     """
-    Delete selected mapset
+    mapsets = [(grassdb, location, mapset)]
+    return delete_mapsets_interactively(guiparent, mapsets)
+
+
+def delete_mapsets_interactively(guiparent, mapsets):
+    """Delete multiple mapsets with user interaction.
+
+    Parameter *mapsets* is a list of tuples (database, location, mapset).
+
+    If PERMANENT or current mapset found, delete operation is not performed.
+
+    Exceptions during deletation are handled in this function.
+
+    Retuns True if there was a change, i.e., all mapsets were successfuly deleted
+    or at least one mapset was deleted. Returns False if one or more mapsets cannot be
+    deleted (see above the possible reasons) or if an error was encountered when
+    deleting the first mapset in the list.
     """
-    if mapset == "PERMANENT":
-        GMessage(
+    genv = gisenv()
+    issues = []
+    deletes = []
+
+    # Check selected mapsets and remember issue.
+    # Each error is reported only once (using elif).
+    for grassdb, location, mapset in mapsets:
+        mapset_path = os.path.join(grassdb, location, mapset)
+        # Check for permanent mapsets
+        if mapset == "PERMANENT":
+            issue = _("<{}> is required for a valid location.").format(mapset_path)
+            issues.append(issue)
+        # Check for current mapset
+        elif (
+                grassdb == genv['GISDBASE'] and
+                location == genv['LOCATION_NAME'] and
+                mapset == genv['MAPSET']
+        ):
+            issue = _("<{}> is the current mapset.").format(mapset_path)
+            issues.append(issue)
+        # No issue detected
+        else:
+            deletes.append(mapset_path)
+
+    modified = False  # True after first successful delete
+    # If any issues, display the warning message and do not delete anything
+    if issues:
+        issues = "\n".join(issues)
+        dlg = wx.MessageDialog(
             parent=guiparent,
             message=_(
-                "Mapset <PERMANENT> is required for valid GRASS location.\n\n"
-                "This mapset cannot be deleted."
-            ),
+                "Cannot delete one or more mapsets for the following reasons:\n\n"
+                "{}\n\n"
+                "No mapsets will be deleted."
+            ).format(issues),
+            caption=_("Unable to delete selected mapsets"),
+            style=wx.OK | wx.ICON_WARNING
         )
-        return False
-
-    dlg = wx.MessageDialog(
-        parent=guiparent,
-        message=_(
-            "Do you want to continue with deleting mapset <%(mapset)s> "
-            "from location <%(location)s>?\n\n"
-            "ALL MAPS included in this mapset will be "
-            "PERMANENTLY DELETED!"
+        dlg.ShowModal()
+    else:
+        deletes = "\n".join(deletes)
+        dlg = wx.MessageDialog(
+            parent=guiparent,
+            message=_(
+                "Do you want to continue with deleting"
+                " one or more of the following mapsets?\n\n"
+                "{}\n\n"
+                "All maps included in these mapsets will be permanently deleted!"
+            ).format(deletes),
+            caption=_("Delete selected mapsets"),
+            style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION,
         )
-        % {"mapset": mapset, "location": location},
-        caption=_("Delete selected mapset"),
-        style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION,
-    )
-
-    if dlg.ShowModal() == wx.ID_YES:
-        try:
-            delete_mapset(grassdb, location, mapset)
-            dlg.Destroy()
-            return True
-        except OSError as err:
-            wx.MessageBox(
-                parent=guiparent,
-                caption=_("Error"),
-                message=_("Unable to delete mapset.\n\n%s") % err,
-                style=wx.OK | wx.ICON_ERROR | wx.CENTRE,
-            )
+        if dlg.ShowModal() == wx.ID_YES:
+            try:
+                for grassdb, location, mapset in mapsets:
+                    delete_mapset(grassdb, location, mapset)
+                    modified = True
+                dlg.Destroy()
+                return modified
+            except OSError as error:
+                wx.MessageBox(
+                    parent=guiparent,
+                    caption=_("Error when deleting mapsets"),
+                    message=_(
+                        "The following error occured when deleting mapset <{path}>:"
+                        "\n\n{error}\n\n"
+                        "Deleting of mapsets was interrupted."
+                        ).format(
+                            path=os.path.join(grassdb, location, mapset),
+                            error=error,
+                    ),
+                    style=wx.OK | wx.ICON_ERROR | wx.CENTRE,
+                )
     dlg.Destroy()
-    return False
+    return modified
 
 
 def delete_location_interactively(guiparent, grassdb, location):
@@ -409,11 +467,10 @@ def delete_location_interactively(guiparent, grassdb, location):
         parent=guiparent,
         message=_(
             "Do you want to continue with deleting "
-            "location <%s>?\n\n"
+            "location {}?\n\n"
             "ALL MAPS included in this location will be "
             "PERMANENTLY DELETED!"
-        )
-        % (location),
+        ).format(location),
         caption=_("Delete selected location"),
         style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION,
     )
@@ -427,7 +484,7 @@ def delete_location_interactively(guiparent, grassdb, location):
             wx.MessageBox(
                 parent=guiparent,
                 caption=_("Error"),
-                message=_("Unable to delete location.\n\n%s") % err,
+                message=_("Unable to delete location.\n\n{}").format(err),
                 style=wx.OK | wx.ICON_ERROR | wx.CENTRE,
             )
     dlg.Destroy()