Parcourir la source

wxGUI datacatalog: add direct editing of mapset and location name (#920)

addresses both #917, #918. 

Co-authored-by: Anna Petrasova <kratochanna@gmail.com>
Linda Kladivova il y a 4 ans
Parent
commit
80538be4d6
3 fichiers modifiés avec 343 ajouts et 211 suppressions
  1. 82 11
      gui/wxpython/datacatalog/tree.py
  2. 30 193
      gui/wxpython/startup/guiutils.py
  3. 231 7
      lib/python/grassdb/checks.py

+ 82 - 11
gui/wxpython/datacatalog/tree.py

@@ -46,7 +46,15 @@ from startup.guiutils import (
     download_location_interactively,
     delete_grassdb_interactively,
     can_switch_mapset_interactive,
-    switch_mapset_interactively
+    switch_mapset_interactively,
+    get_reason_mapset_not_removable,
+    get_reasons_location_not_removable,
+    get_mapset_name_invalid_reason,
+    get_location_name_invalid_reason
+)
+from grass.grassdb.manage import (
+    rename_mapset,
+    rename_location
 )
 
 from grass.pydispatch.signal import Signal
@@ -854,22 +862,85 @@ class DataCatalogTree(TreeView):
     def OnStartEditLabel(self, node, event):
         """Start label editing"""
         self.DefineItems([node])
-        # TODO: add renaming mapset/location
-        if not self.selected_layer[0]:
+
+        # Not allowed for grassdb node
+        if node.data['type'] == 'grassdb':
             event.Veto()
-            return
-        Debug.msg(1, "Start label edit {name}".format(name=node.data['name']))
-        label = _("Editing {name}").format(name=node.data['name'])
-        self.showNotification.emit(message=label)
+        # Check selected mapset
+        elif node.data['type'] == 'mapset':
+            if (
+                self._restricted
+                or get_reason_mapset_not_removable(self.selected_grassdb[0].data['name'],
+                                                   self.selected_location[0].data['name'],
+                                                   self.selected_mapset[0].data['name'],
+                                                   check_permanent=True)
+            ):
+                event.Veto()
+        # Check selected location
+        elif node.data['type'] == 'location':
+            if (
+                self._restricted
+                or get_reasons_location_not_removable(self.selected_grassdb[0].data['name'],
+                                                      self.selected_location[0].data['name'])
+            ):
+                event.Veto()
+        elif node.data['type'] in ('raster', 'raster_3d', 'vector'):
+            currentGrassDb, currentLocation, currentMapset = self._isCurrent(gisenv())
+            if not currentMapset:
+                event.Veto()
 
     def OnEditLabel(self, node, event):
         """End label editing"""
-        if self.selected_layer and not event.IsEditCancelled():
-            old_name = node.data['name']
-            Debug.msg(1, "End label edit {name}".format(name=old_name))
-            new_name = event.GetLabel()
+        if event.IsEditCancelled():
+            return
+
+        old_name = node.data['name']
+        Debug.msg(1, "End label edit {name}".format(name=old_name))
+        new_name = event.GetLabel()
+
+        if node.data['type'] in ('raster', 'raster_3d', 'vector'):
             self.Rename(old_name, new_name)
 
+        elif node.data['type'] == 'mapset':
+            message = get_mapset_name_invalid_reason(
+                            self.selected_grassdb[0].data['name'],
+                            self.selected_location[0].data['name'],
+                            new_name)
+            if message:
+                GError(parent=self, message=message,
+                       caption=_("Cannot rename mapset"),
+                       showTraceback=False)
+                event.Veto()
+                return
+            rename_mapset(self.selected_grassdb[0].data['name'],
+                          self.selected_location[0].data['name'],
+                          self.selected_mapset[0].data['name'],
+                          new_name)
+            self._renameNode(self.selected_mapset[0], new_name)
+            label = _(
+                "Renaming mapset <{oldmapset}> to <{newmapset}> completed").format(
+                oldmapset=old_name, newmapset=new_name)
+            self.showNotification.emit(message=label)
+
+        elif node.data['type'] == 'location':
+            message = get_location_name_invalid_reason(
+                            self.selected_grassdb[0].data['name'],
+                            new_name)
+            if message:
+                GError(parent=self, message=message,
+                       caption=_("Cannot rename location"),
+                       showTraceback=False)
+                event.Veto()
+                return
+            rename_location(self.selected_grassdb[0].data['name'],
+                            self.selected_location[0].data['name'],
+                            new_name)
+            self._renameNode(self.selected_location[0], new_name)
+            label = _(
+                "Renaming location <{oldlocation}> to <{newlocation}> completed").format(
+                oldlocation=old_name, newlocation=new_name)
+            self.showNotification.emit(message=label)
+
     def Rename(self, old, new):
         """Rename layer"""
         string = old + ',' + new

+ 30 - 193
gui/wxpython/startup/guiutils.py

@@ -19,16 +19,18 @@ import os
 import sys
 import wx
 
-import grass.script as gs
-from grass.script import gisenv
 from grass.grassdb.checks import (
-    mapset_exists,
-    location_exists,
     is_mapset_locked,
     get_mapset_lock_info,
-    is_different_mapset_owner,
-    is_mapset_current,
-    is_location_current
+    is_mapset_name_valid,
+    is_location_name_valid,
+    get_mapset_name_invalid_reason,
+    get_location_name_invalid_reason,
+    get_reason_mapset_not_removable,
+    get_reasons_mapsets_not_removable,
+    get_reasons_location_not_removable,
+    get_reasons_locations_not_removable,
+    get_reasons_grassdb_not_removable
 )
 
 from grass.grassdb.create import create_mapset, get_default_mapset_name
@@ -40,12 +42,11 @@ from grass.grassdb.manage import (
     rename_location,
 )
 
-from core.utils import GetListOfLocations
 from core import globalvar
 from core.gcmd import GError, GMessage, DecodeString, RunCommand
 from gui_core.dialogs import TextEntryDialog
 from location_wizard.dialogs import RegionDef
-from gui_core.widgets import GenericMultiValidator
+from gui_core.widgets import GenericValidator
 
 
 def SetSessionMapset(database, location, mapset):
@@ -61,11 +62,8 @@ class MapsetDialog(TextEntryDialog):
         self.database = database
         self.location = location
 
-        # list of tuples consisting of conditions and callbacks
-        checks = [(gs.legal_name, self._nameValidationFailed),
-                  (self._checkMapsetNotExists, self._mapsetAlreadyExists),
-                  (self._checkOGR, self._reservedMapsetName)]
-        validator = GenericMultiValidator(checks)
+        validator = GenericValidator(self._isMapsetNameValid,
+                                     self._showMapsetNameInvalidReason)
 
         TextEntryDialog.__init__(
             self, parent=parent,
@@ -75,39 +73,15 @@ class MapsetDialog(TextEntryDialog):
             validator=validator,
         )
 
-    def _nameValidationFailed(self, ctrl):
-        message = _(
-            "Name '{}' is not a valid name for location or mapset. "
-            "Please use only ASCII characters excluding characters {} "
-            "and space.").format(ctrl.GetValue(), '/"\'@,=*~')
-        GError(parent=self, message=message, caption=_("Invalid name"))
-
-    def _checkOGR(self, text):
-        """Check user's input for reserved mapset name."""
-        if text.lower() == 'ogr':
-            return False
-        return True
-
-    def _reservedMapsetName(self, ctrl):
-        message = _(
-            "Name '{}' is reserved for direct "
-            "read access to OGR layers. Please use "
-            "another name for your mapset.").format(ctrl.GetValue())
-        GError(parent=self, message=message,
-               caption=_("Reserved mapset name"))
-
-    def _checkMapsetNotExists(self, text):
-        """Check whether user's input mapset exists or not."""
-        if mapset_exists(self.database, self.location, text):
-            return False
-        return True
-
-    def _mapsetAlreadyExists(self, ctrl):
-        message = _(
-            "Mapset '{}' already exists. Please consider using "
-            "another name for your mapset.").format(ctrl.GetValue())
-        GError(parent=self, message=message,
-               caption=_("Existing mapset path"))
+    def _showMapsetNameInvalidReason(self, ctrl):
+        message = get_mapset_name_invalid_reason(self.database,
+                                                 self.location,
+                                                 ctrl.GetValue())
+        GError(parent=self, message=message, caption=_("Invalid mapset name"))
+
+    def _isMapsetNameValid(self, text):
+        """Check whether user's input location is valid or not."""
+        return is_mapset_name_valid(self.database, self.location, text)
 
 
 class LocationDialog(TextEntryDialog):
@@ -115,10 +89,8 @@ class LocationDialog(TextEntryDialog):
                  database=None):
         self.database = database
 
-        # list of tuples consisting of conditions and callbacks
-        checks = [(gs.legal_name, self._nameValidationFailed),
-                  (self._checkLocationNotExists, self._locationAlreadyExists)]
-        validator = GenericMultiValidator(checks)
+        validator = GenericValidator(self._isLocationNameValid,
+                                     self._showLocationNameInvalidReason)
 
         TextEntryDialog.__init__(
             self, parent=parent,
@@ -128,149 +100,14 @@ class LocationDialog(TextEntryDialog):
             validator=validator,
         )
 
-    def _nameValidationFailed(self, ctrl):
-        message = _(
-            "Name '{}' is not a valid name for location or mapset. "
-            "Please use only ASCII characters excluding characters {} "
-            "and space.").format(ctrl.GetValue(), '/"\'@,=*~')
-        GError(parent=self, message=message, caption=_("Invalid name"))
-
-    def _checkLocationNotExists(self, text):
-        """Check whether user's input location exists or not."""
-        if location_exists(self.database, text):
-            return False
-        return True
-
-    def _locationAlreadyExists(self, ctrl):
-        message = _(
-            "Location '{}' already exists. Please consider using "
-            "another name for your location.").format(ctrl.GetValue())
-        GError(parent=self, message=message,
-               caption=_("Existing location path"))
-
-
-def get_reasons_mapsets_not_removable(mapsets, check_permanent):
-    """Get reasons why mapsets cannot be removed.
-
-    Parameter *mapsets* is a list of tuples (database, location, mapset).
-    Parameter *check_permanent* is True of False. It depends on whether
-    we want to check for permanent mapset or not.
-
-    Returns messages as list if there were any failed checks, otherwise empty list.
-    """
-    messages = []
-    for grassdb, location, mapset in mapsets:
-        message = get_reason_mapset_not_removable(grassdb, location,
-                                                  mapset, check_permanent)
-        if message:
-            messages.append(message)
-    return messages
-
-
-def get_reason_mapset_not_removable(grassdb, location, mapset, check_permanent):
-    """Get reason why one mapset cannot be removed.
-
-    Parameter *check_permanent* is True of False. It depends on whether
-    we want to check for permanent mapset or not.
-
-    Returns message as string if there was failed check, otherwise None.
-    """
-    message = None
-    mapset_path = os.path.join(grassdb, location, mapset)
-
-    # Check if mapset is permanent
-    if check_permanent and mapset == "PERMANENT":
-        message = _("Mapset <{mapset}> is required for a valid location.").format(
-            mapset=mapset_path)
-    # Check if mapset is current
-    elif is_mapset_current(grassdb, location, mapset):
-        message = _("Mapset <{mapset}> is the current mapset.").format(
-            mapset=mapset_path)
-    # Check whether mapset is in use
-    elif is_mapset_locked(mapset_path):
-        message = _("Mapset <{mapset}> is in use.").format(
-            mapset=mapset_path)
-    # Check whether mapset is owned by different user
-    elif is_different_mapset_owner(mapset_path):
-        message = _("Mapset <{mapset}> is owned by a different user.").format(
-            mapset=mapset_path)
-
-    return message
-
-
-def get_reasons_locations_not_removable(locations):
-    """Get reasons why locations cannot be removed.
-
-    Parameter *locations* is a list of tuples (database, location).
-
-    Returns messages as list if there were any failed checks, otherwise empty list.
-    """
-    messages = []
-    for grassdb, location in locations:
-        messages += get_reasons_location_not_removable(grassdb, location)
-    return messages
-
-
-def get_reasons_location_not_removable(grassdb, location):
-    """Get reasons why one location cannot be removed.
-
-    Returns messages as list if there were any failed checks, otherwise empty list.
-    """
-    messages = []
-    location_path = os.path.join(grassdb, location)
-
-    # Check if location is current
-    if is_location_current(grassdb, location):
-        messages.append(_("Location <{location}> is the current location.").format(
-            location=location_path))
-        return messages
-
-    # Find mapsets in particular location
-    tmp_gisrc_file, env = gs.create_environment(grassdb, location, 'PERMANENT')
-    env['GRASS_SKIP_MAPSET_OWNER_CHECK'] = '1'
-
-    g_mapsets = gs.read_command(
-        'g.mapsets',
-        flags='l',
-        separator='comma',
-        quiet=True,
-        env=env).strip().split(',')
-
-    # Append to the list of tuples
-    mapsets = []
-    for g_mapset in g_mapsets:
-        mapsets.append((grassdb, location, g_mapset))
-
-    # Concentenate both checks
-    messages += get_reasons_mapsets_not_removable(mapsets, check_permanent=False)
-
-    gs.try_remove(tmp_gisrc_file)
-    return messages
-
-
-def get_reasons_grassdb_not_removable(grassdb):
-    """Get reasons why one grassdb cannot be removed.
-
-    Returns messages as list if there were any failed checks, otherwise empty list.
-    """
-    messages = []
-    genv = gisenv()
-
-    # Check if grassdb is current
-    if grassdb == genv['GISDBASE']:
-        messages.append(_("GRASS database <{grassdb}> is the current database.").format(
-            grassdb=grassdb))
-        return messages
-
-    g_locations = GetListOfLocations(grassdb)
-
-    # Append to the list of tuples
-    locations = []
-    for g_location in g_locations:
-        locations.append((grassdb, g_location))
-    messages = get_reasons_locations_not_removable(locations)
+    def _showLocationNameInvalidReason(self, ctrl):
+        message = get_location_name_invalid_reason(self.database,
+                                                   ctrl.GetValue())
+        GError(parent=self, message=message, caption=_("Invalid location name"))
 
-    return messages
+    def _isLocationNameValid(self, text):
+        """Check whether user's input location is valid or not."""
+        return is_location_name_valid(self.database, text)
 
 
 # TODO: similar to (but not the same as) read_gisrc function in grass.py

+ 231 - 7
lib/python/grassdb/checks.py

@@ -14,6 +14,8 @@ import os
 import datetime
 from pathlib import Path
 from grass.script import gisenv
+import grass.script as gs
+import glob
 
 
 def mapset_exists(database, location, mapset):
@@ -60,19 +62,19 @@ def is_location_valid(database, location):
     )
 
 
-def is_mapset_current(grassdb, location, mapset):
+def is_mapset_current(database, location, mapset):
     genv = gisenv()
-    if (grassdb == genv['GISDBASE'] and
-        location == genv['LOCATION_NAME'] and
-        mapset == genv['MAPSET']):
+    if (database == genv['GISDBASE'] and
+            location == genv['LOCATION_NAME'] and
+            mapset == genv['MAPSET']):
         return True
     return False
 
 
-def is_location_current(grassdb, location):
+def is_location_current(database, location):
     genv = gisenv()
-    if (grassdb == genv['GISDBASE'] and
-        location == genv['LOCATION_NAME']):
+    if (database == genv['GISDBASE'] and
+            location == genv['LOCATION_NAME']):
         return True
     return False
 
@@ -305,3 +307,225 @@ def get_location_invalid_suggestion(database, location):
             " Did you mean to specify one of them?"
         ).format(location=location)
     return None
+
+
+def get_mapset_name_invalid_reason(database, location, mapset_name):
+    """Get reasons why mapset name is not valid.
+
+    It gets reasons when:
+     * Name is not valid.
+     * Name is reserved for OGR layers.
+     * Mapset in the same path already exists.
+
+    Returns message as string if there was a reason, otherwise None.
+    """
+    message = None
+    mapset_path = os.path.join(database, location, mapset_name)
+
+    # Check if mapset name is valid
+    if not gs.legal_name(mapset_name):
+        message = _(
+            "Name '{}' is not a valid name for location or mapset. "
+            "Please use only ASCII characters excluding characters {} "
+            "and space.").format(mapset_name, '/"\'@,=*~')
+    # Check reserved mapset name
+    elif mapset_name.lower() == 'ogr':
+        message = _(
+            "Name '{}' is reserved for direct "
+            "read access to OGR layers. Please use "
+            "another name for your mapset.").format(mapset_name)
+    # Check whether mapset exists
+    elif mapset_exists(database, location, mapset_name):
+        message = _(
+            "Mapset  <{mapset}> already exists. Please consider using "
+            "another name for your mapset.").format(mapset=mapset_path)
+
+    return message
+
+
+def get_location_name_invalid_reason(grassdb, location_name):
+    """Get reasons why location name is not valid.
+
+    It gets reasons when:
+     * Name is not valid.
+     * Location in the same path already exists.
+
+    Returns message as string if there was a reason, otherwise None.
+    """
+    message = None
+    location_path = os.path.join(grassdb, location_name)
+
+    # Check if mapset name is valid
+    if not gs.legal_name(location_name):
+        message = _(
+            "Name '{}' is not a valid name for location or mapset. "
+            "Please use only ASCII characters excluding characters {} "
+            "and space.").format(location_name, '/"\'@,=*~')
+    # Check whether location exists
+    elif location_exists(grassdb, location_name):
+        message = _(
+            "Location  <{location}> already exists. Please consider using "
+            "another name for your location.").format(location=location_path)
+
+    return message
+
+
+def is_mapset_name_valid(database, location, mapset_name):
+    """Check if mapset name is valid.
+
+    Returns True if mapset name is valid, otherwise False.
+    """
+    return gs.legal_name(mapset_name) and mapset_name.lower() != "ogr" and not \
+        mapset_exists(database, location, mapset_name)
+
+
+def is_location_name_valid(database, location_name):
+    """Check if location name is valid.
+
+    Returns True if location name is valid, otherwise False.
+    """
+    return gs.legal_name(location_name) and not \
+        location_exists(database, location_name)
+
+
+def get_reasons_mapsets_not_removable(mapsets, check_permanent):
+    """Get reasons why mapsets cannot be removed.
+
+    Parameter *mapsets* is a list of tuples (database, location, mapset).
+    Parameter *check_permanent* is True of False. It depends on whether
+    we want to check for permanent mapset or not.
+
+    Returns messages as list if there were any failed checks, otherwise empty list.
+    """
+    messages = []
+    for grassdb, location, mapset in mapsets:
+        message = get_reason_mapset_not_removable(grassdb, location,
+                                                  mapset, check_permanent)
+        if message:
+            messages.append(message)
+    return messages
+
+
+def get_reason_mapset_not_removable(grassdb, location, mapset, check_permanent):
+    """Get reason why one mapset cannot be removed.
+
+    Parameter *check_permanent* is True of False. It depends on whether
+    we want to check for permanent mapset or not.
+
+    Returns message as string if there was failed check, otherwise None.
+    """
+    message = None
+    mapset_path = os.path.join(grassdb, location, mapset)
+
+    # Check if mapset is permanent
+    if check_permanent and mapset == "PERMANENT":
+        message = _("Mapset <{mapset}> is required for a valid location.").format(
+            mapset=mapset_path)
+    # Check if mapset is current
+    elif is_mapset_current(grassdb, location, mapset):
+        message = _("Mapset <{mapset}> is the current mapset.").format(
+            mapset=mapset_path)
+    # Check whether mapset is in use
+    elif is_mapset_locked(mapset_path):
+        message = _("Mapset <{mapset}> is in use.").format(
+            mapset=mapset_path)
+    # Check whether mapset is owned by different user
+    elif is_different_mapset_owner(mapset_path):
+        message = _("Mapset <{mapset}> is owned by a different user.").format(
+            mapset=mapset_path)
+
+    return message
+
+
+def get_reasons_locations_not_removable(locations):
+    """Get reasons why locations cannot be removed.
+
+    Parameter *locations* is a list of tuples (database, location).
+
+    Returns messages as list if there were any failed checks, otherwise empty list.
+    """
+    messages = []
+    for grassdb, location in locations:
+        messages += get_reasons_location_not_removable(grassdb, location)
+    return messages
+
+
+def get_reasons_location_not_removable(grassdb, location):
+    """Get reasons why one location cannot be removed.
+
+    Returns messages as list if there were any failed checks, otherwise empty list.
+    """
+    messages = []
+    location_path = os.path.join(grassdb, location)
+
+    # Check if location is current
+    if is_location_current(grassdb, location):
+        messages.append(_("Location <{location}> is the current location.").format(
+            location=location_path))
+        return messages
+
+    # Find mapsets in particular location
+    tmp_gisrc_file, env = gs.create_environment(grassdb, location, 'PERMANENT')
+    env['GRASS_SKIP_MAPSET_OWNER_CHECK'] = '1'
+
+    g_mapsets = gs.read_command(
+        'g.mapsets',
+        flags='l',
+        separator='comma',
+        quiet=True,
+        env=env).strip().split(',')
+
+    # Append to the list of tuples
+    mapsets = []
+    for g_mapset in g_mapsets:
+        mapsets.append((grassdb, location, g_mapset))
+
+    # Concentenate both checks
+    messages += get_reasons_mapsets_not_removable(mapsets, check_permanent=False)
+
+    gs.try_remove(tmp_gisrc_file)
+    return messages
+
+
+def get_reasons_grassdb_not_removable(grassdb):
+    """Get reasons why one grassdb cannot be removed.
+
+    Returns messages as list if there were any failed checks, otherwise empty list.
+    """
+    messages = []
+    genv = gisenv()
+
+    # Check if grassdb is current
+    if grassdb == genv['GISDBASE']:
+        messages.append(_("GRASS database <{grassdb}> is the current database.").format(
+            grassdb=grassdb))
+        return messages
+
+    g_locations = get_list_of_locations(grassdb)
+
+    # Append to the list of tuples
+    locations = []
+    for g_location in g_locations:
+        locations.append((grassdb, g_location))
+    messages = get_reasons_locations_not_removable(locations)
+
+    return messages
+
+
+def get_list_of_locations(dbase):
+    """Get list of GRASS locations in given dbase
+
+    :param dbase: GRASS database path
+
+    :return: list of locations (sorted)
+    """
+    locations = list()
+    for location in glob.glob(os.path.join(dbase, "*")):
+        if os.path.join(
+                location, "PERMANENT") in glob.glob(
+                os.path.join(location, "*")):
+            locations.append(os.path.basename(location))
+
+    locations.sort(key=lambda x: x.lower())
+
+    return locations