[PATCH 1/2] Move the add_check stuff into helper classes.

David Shea dshea at redhat.com
Tue Jan 7 15:32:28 UTC 2014


This moves the input checking code out of GUIObject and NormalSpoke and
kills the GUIDialog class. Split the functionality into three classes:
one without anything GUI-specific, one which adds some functionality for
working with GtkEditable widgets, and one which adds some functionality
for working with GUI spokes.
---
 pyanaconda/ui/gui/__init__.py        | 277 -----------------------------------
 pyanaconda/ui/gui/spokes/__init__.py |   7 -
 pyanaconda/ui/gui/spokes/password.py |  53 ++++---
 pyanaconda/ui/gui/spokes/user.py     | 106 +++++++-------
 pyanaconda/ui/helpers.py             | 234 ++++++++++++++++++++++++++++-
 5 files changed, 313 insertions(+), 364 deletions(-)

diff --git a/pyanaconda/ui/gui/__init__.py b/pyanaconda/ui/gui/__init__.py
index e2e0cfc..38f35cf 100644
--- a/pyanaconda/ui/gui/__init__.py
+++ b/pyanaconda/ui/gui/__init__.py
@@ -42,96 +42,6 @@ SCREENSHOT_DELAY = 1  # in seconds
 
 ANACONDA_WINDOW_GROUP = Gtk.WindowGroup()
 
-class GUICheck(object):
-    """Handle an input validation check."""
-
-    # Use as a return value to indicate a passed check
-    CHECK_OK = None
-
-    def __init__(self, parent, editable, run_check, check_data, set_error):
-        """Create a new input validation check.
-
-           :param parent: The parent GUIObject. When a check state changes,
-                          the GUICheck will call set_error(check, check-state)
-           :type parent:  GUIObject
-
-           :param editable: The input field being checked
-           :type editable:  GtkEditable
-
-           :param run_check: The check function. The function is called as
-                             check(editable, check_data). The return value is an
-                             error state object or CHECK_OK if the check succeeds.
-           :type run_check:  function
-
-           :param check_data: An optional parameter passed to check().
-
-           :param set_error: A function called when the state of this check
-                             changes. The parameters are (GUICheck, run_check_return).
-                             The return value is ignored.
-           :type set_error:  function
-        """
-
-        self._parent = parent
-        self._editable = editable
-        self._run_check = run_check
-        self._check_data = check_data
-        self._set_error = set_error
-
-        # Set to the Gtk handler ID in enable()
-        self._handler_id = None
-
-        # Initial check state
-        self._check_status = None
-
-        self.enable()
-
-    def enable(self):
-        """Enable the check.
-
-           enable() does not check the current state of the input field. To
-           check the current state, run update_check_status() after enable().
-        """
-        if not self._handler_id:
-            self._handler_id = self._editable.connect_after("changed", self.update_check_status)
-
-    def disable(self):
-        """Disable the check. The check will no longer appear in failed_checks,
-           but disabling the check does not call set_error to update the
-           GUIObject's state.
-        """
-        if self._handler_id:
-            self._editable.disconnect(self._handler_id)
-            self._handler_id = None
-            self._check_status = None
-
-    def update_check_status(self, editable=None, check_data=None):
-        """Run an input validation check."""
-
-        # Allow check parameters to be overriden in parameters
-        if editable is None:
-            editable = self._editable
-        if check_data is None:
-            check_data = self._check_data
-
-        new_check_status = self._run_check(editable, check_data)
-        check_status_changed = (self._check_status != new_check_status)
-        self._check_status = new_check_status
-
-        if check_status_changed:
-            self._set_error(self, self._check_status)
-
-    @property
-    def check_status(self):
-        return self._check_status
-
-    @property
-    def editable(self):
-        return self._editable
-
-    @property
-    def check_data(self):
-        return self._check_data
-
 class GUIObject(common.UIObject):
     """This is the base class from which all other GUI classes are derived.  It
        thus contains only attributes and methods that are common to everything
@@ -214,8 +124,6 @@ class GUIObject(common.UIObject):
         Keybinder.init()
         Keybinder.bind("<Shift>Print", self._handlePrntScreen, [])
 
-        self._check_list = []
-
     def _findUIFile(self):
         path = os.environ.get("UIPATH", "./:/tmp/updates/:/tmp/updates/ui/:/usr/share/anaconda/ui/")
         dirs = path.split(":")
@@ -294,173 +202,6 @@ class GUIObject(common.UIObject):
         """
         self.window.set_warning(msg)
 
-    def add_check(self, editable, run_check, check_data=None, set_error=None):
-        """Add an input validation check to this object.
-
-           This function creates new GUICheck object and adds it to this
-           GUIObject. The check is run any time the input field changes.
-           If the result of a check changes, the check object will call
-           the set_error function. By default, set_error will call
-           self.set_warning with the status of the first failed check.
-
-           :param editable: the input field to validate
-           :type editable: GtkEditable
-
-           :param run_check: a function called to validate the input field. The
-                         parameters are (editable, check_data). The return
-                         value is an error state object or GUICheck.CHECK_OK if
-                         the check passes.
-           :type run_check: function
-
-           :param check_data: additional data to pass to the check function
-
-           :param set_error: a function called when a check changes state. The
-                         parameters are (GUICheck, run_check_return).  The
-                         return value is ignored.
-           :type set_error: function
-
-           :returns: A check object
-           :rtype: GUICheck
-        """
-
-        if not set_error:
-            set_error = self.set_check_error
-
-        checkRef = GUICheck(self, editable, run_check, check_data, set_error)
-        self._check_list.append(checkRef)
-        return checkRef
-
-    def add_re_check(self, editable, regex, message, set_error=None):
-        """Add a check using a regular expresion.
-
-           :param editable: the input field to validate
-           :type editable:  GtkEditable
-
-           :param regex: the regular expression to use to check the input
-           :type regex:  re.RegexObject
-
-           :param message: The message to set if the regex does not match
-           :type message:  str
-
-           :param set_error: a function called when a check changes state. The
-                         parameters are (GUICheck, run_check_return).  The
-                         return value is ignored.
-           :type set_error: function
-
-           :returns: A check object
-           :rtype: GUICheck
-        """
-        if not set_error:
-            set_error = self.set_check_error
-        return self.add_check(editable=editable, run_check=check_re,
-                check_data={'regex': regex, 'message': message}, set_error=set_error)
-
-    def set_check_error(self, check, check_return):
-        """Update the warning with the input validation check error."""
-        # Grab the first failed check
-        failed_check = next(self.failed_checks, None)
-
-        self.clear_info()
-        if failed_check:
-            self.set_warning(failed_check.check_status)
-            self.window.show_all()
-
-    @property
-    def failed_checks(self):
-        """A generator of all failed input checks"""
-        return (c for c in self._check_list if c.check_status)
-
-    @property
-    def checks(self):
-        """An iterator over all input checks"""
-        return self._check_list.__iter__()
-
-class GUIDialog(GUIObject):
-    """This is an abstract for creating dialog windows. It displays an error
-       message when an input validation fails.
-
-       GUIDialog does not define where errors are displayed, so classes
-       that derive from GUIDialog must define error labels and include them
-       as the check_data parameter to add_check. More than one check can use
-       the same label: the message from the first failed check will update the
-       label.
-    """
-
-    def __init__(self, data):
-        if self.__class__ is GUIDialog:
-            raise TypeError("GUIDialog is an abstract class")
-
-        GUIObject.__init__(self, data)
-
-    def add_check_with_error_label(self, editable, error_label, run_check,
-            check_data=None, set_error=None):
-        """Add an input validation check to this dialog. The error_label will
-           be added to the check_data for the validation check and will be
-           used to display the error message if the check fails.
-
-           :param editable: the input field to validate
-           :type editable: GtkEditable
-
-           :param error_label: the label in which to display the error data
-           :type error_label:  GtkLabel
-
-           :param run_check: a function called to validate the input field. The
-                         parameters are (editable, check_data). The return
-                         value is an error state object or GUICheck.CHECK_OK if
-                         the check passes.
-           :type run_check: function
-
-           :param check_data: additional data to pass to the check function
-
-           :param set_error: a function called when a check changes state. The
-                         parameters are (GUICheck, run_check_return).  The
-                         return value is ignored.
-           :type set_error: function
-
-           :returns: A check object
-           :rtype: GUICheck
-        """
-        if not set_error:
-            set_error = self.set_check_error
-
-        return self.add_check(editable=editable, run_check=run_check,
-                check_data={'error_label': error_label, 'message': check_data},
-                set_error=set_error)
-
-    def add_re_check_with_error_label(self, editable, error_label, regex, message, set_error=None):
-        """Add a check using a regular expression."""
-        # Use the GUIObject function so we can create the check_data dictionary here
-        if not set_error:
-            set_error = self.set_check_error
-
-        return self.add_check(editable=editable, run_check=check_re,
-                check_data={'error_label': error_label, 'message': message, 'regex': regex},
-                set_error=set_error)
-
-    def set_check_error(self, check, check_return):
-        """Update all input check failure messages.
-
-           If multiple checks use the same GtkLabel, only the first one will
-           be used.
-        """
-
-        # If the signaling check passed, clear its error label
-        if not check_return:
-            if 'error_label' in check.check_data:
-                check.check_data['error_label'].set_text('')
-
-        # Keep track of which labels have errors set. If we see an error for
-        # a label that's already been set, skip it.
-        labels_seen = []
-        for failed_check in self.failed_checks:
-            if not 'error_label' in failed_check.check_data:
-                continue
-
-            label = failed_check.check_data['error_label']
-            if label not in labels_seen:
-                labels_seen.append(label)
-                label.set_text(failed_check.check_status)
-
 class QuitDialog(GUIObject):
     builderObjects = ["quitDialog"]
     mainWidgetName = "quitDialog"
@@ -789,21 +530,3 @@ class GraphicalExceptionHandlingIface(meh.ui.gui.GraphicalIntf):
         unbusyCursor()
 
         return exc_window
-
-def check_re(editable, data):
-    """Perform an input validation check against a regular expression.
-
-       :param editable: The input field being checked
-       :type editable:  GtkEditable
-
-       :param data: The check_data set in add_check. This data must
-                    be a dictionary that includes the keys
-                    'regex' and 'message'.
-       :type data:  dict
-
-       :returns: error_data if the check fails, otherwise GUICheck.CHECK_OK.
-    """
-    if data['regex'].match(editable.get_text()):
-        return GUICheck.CHECK_OK
-    else:
-        return data['message']
diff --git a/pyanaconda/ui/gui/spokes/__init__.py b/pyanaconda/ui/gui/spokes/__init__.py
index cc91ef6..0cd44b3 100644
--- a/pyanaconda/ui/gui/spokes/__init__.py
+++ b/pyanaconda/ui/gui/spokes/__init__.py
@@ -91,13 +91,6 @@ class NormalSpoke(Spoke, common.NormalSpoke):
     def on_back_clicked(self, window):
         from gi.repository import Gtk
 
-        # Look for failed checks
-        failed_check = next(self.failed_checks, None)
-        if failed_check:
-            # Set the focus to the first failed check and stay in the spoke
-            failed_check.editable.grab_focus()
-            return
-
         self.window.hide()
         Gtk.main_quit()
 
diff --git a/pyanaconda/ui/gui/spokes/password.py b/pyanaconda/ui/gui/spokes/password.py
index b66e2d5..0246643 100644
--- a/pyanaconda/ui/gui/spokes/password.py
+++ b/pyanaconda/ui/gui/spokes/password.py
@@ -22,10 +22,10 @@
 from pyanaconda.i18n import _, CN_
 from pyanaconda.users import cryptPassword, validatePassword
 
-from pyanaconda.ui.gui import GUICheck
 from pyanaconda.ui.gui.spokes import NormalSpoke
 from pyanaconda.ui.gui.categories.user_settings import UserSettingsCategory
 from pyanaconda.ui.common import FirstbootSpokeMixIn
+from pyanaconda.ui.helpers import GUISpokeInputCheckHandler, InputCheck
 
 from pyanaconda.constants import PASSWORD_EMPTY_ERROR, PASSWORD_CONFIRM_ERROR_GUI,\
         PASSWORD_STRENGTH_DESC, PASSWORD_WEAK, PASSWORD_WEAK_WITH_ERROR,\
@@ -34,7 +34,7 @@ from pyanaconda.constants import PASSWORD_EMPTY_ERROR, PASSWORD_CONFIRM_ERROR_GU
 __all__ = ["PasswordSpoke"]
 
 
-class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
+class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler):
     builderObjects = ["passwordWindow"]
 
     mainWidgetName = "passwordWindow"
@@ -47,6 +47,7 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
 
     def __init__(self, *args):
         NormalSpoke.__init__(self, *args)
+        GUISpokeInputCheckHandler.__init__(self)
         self._kickstarted = False
 
     def initialize(self):
@@ -100,7 +101,7 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
     def refresh(self):
         # Enable the input checks in case they were disabled on the last exit
         for check in self.checks:
-            check.enable()
+            check.enabled = True
 
         self.pw.grab_focus()
         self.pw.emit("changed")
@@ -140,47 +141,44 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
     def completed(self):
         return bool(self.data.rootpw.password or self.data.rootpw.lock)
 
-    def _checkPasswordEmpty(self, editable, data):
+    def _checkPasswordEmpty(self, inputcheck):
         """Check whether a password has been specified at all."""
 
         # If the password was set by kickstart, skip this check
         if self._kickstarted:
-            return GUICheck.CHECK_OK
+            return InputCheck.CHECK_OK
 
-        if not editable.get_text():
-            if editable == self.pw:
+        if not self.get_input(inputcheck.input_obj):
+            if inputcheck.input_obj == self.pw:
                 return _(PASSWORD_EMPTY_ERROR)
             else:
                 return _(PASSWORD_CONFIRM_ERROR_GUI)
         else:
-            return GUICheck.CHECK_OK
+            return InputCheck.CHECK_OK
 
-    def _checkPasswordConfirm(self, editable=None, reset_status=None):
+    def _checkPasswordConfirm(self, inputcheck):
         """Check whether the password matches the confirmation data."""
 
-        # This check is triggered by changes to either the password field or the
-        # confirmation field. If this method is being run from a successful check
-        # to reset the status, just return success
-        if reset_status:
-            return GUICheck.CHECK_OK
-
         pw = self.pw.get_text()
         confirm = self.confirm.get_text()
 
         # Skip the check if no password is required
         if (not pw and not confirm) and self._kickstarted:
-            result = GUICheck.CHECK_OK
+            result = InputCheck.CHECK_OK
         elif confirm and (pw != confirm):
             result = _(PASSWORD_CONFIRM_ERROR_GUI)
         else:
-            result = GUICheck.CHECK_OK
+            result = InputCheck.CHECK_OK
 
         # If the check succeeded, reset the status of the other check object
-        if result == GUICheck.CHECK_OK:
-            if editable == self.confirm:
-                self._password_check.update_check_status(check_data=True)
+        # Disable the current check to prevent a cycle
+        inputcheck.enabled = False
+        if result == InputCheck.CHECK_OK:
+            if inputcheck == self._confirm_check:
+                self._password_check.update_check_status()
             else:
-                self._confirm_check.update_check_status(check_data=True)
+                self._confirm_check.update_check_status()
+        inputcheck.enabled = True
 
         return result
 
@@ -213,7 +211,7 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
         self.pw_bar.set_value(val)
         self.pw_label.set_text(text)
 
-    def _checkPasswordStrength(self, editable=None, data=None):
+    def _checkPasswordStrength(self, inputcheck):
         """Update the error message based on password strength.
 
            Convert the strength set by _updatePwQuality into an error message.
@@ -224,7 +222,7 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
 
         # Skip the check if no password is required
         if (not pw and not confirm) and self._kickstarted:
-            return GUICheck.CHECK_OK
+            return InputCheck.CHECK_OK
 
         # Check for validity errors
         if (not self._pwq_valid) and (self._pwq_error):
@@ -235,7 +233,7 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
         if pwstrength < 2:
             # If Done has been clicked twice, waive the check
             if self._waivePasswordClicks > 1:
-                return GUICheck.CHECK_OK
+                return InputCheck.CHECK_OK
             elif self._waivePasswordClicks == 1:
                 if self._pwq_error:
                     return _(PASSWORD_WEAK_CONFIRM_WITH_ERROR) % self._pwq_error
@@ -247,7 +245,7 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
                 else:
                     return _(PASSWORD_WEAK)
         else:
-            return GUICheck.CHECK_OK
+            return InputCheck.CHECK_OK
 
     def on_back_clicked(self, button):
         # Add a click and re-check the password strength
@@ -257,6 +255,7 @@ class PasswordSpoke(FirstbootSpokeMixIn, NormalSpoke):
         # If neither the password nor the confirm field are set, skip the checks
         if (not self.pw.get_text()) and (not self.confirm.get_text()):
             for check in self.checks:
-                check.disable()
+                check.enabled = False
 
-        NormalSpoke.on_back_clicked(self, button)
+        if GUISpokeInputCheckHandler.on_back_clicked(self, button):
+            NormalSpoke.on_back_clicked(self, button)
diff --git a/pyanaconda/ui/gui/spokes/user.py b/pyanaconda/ui/gui/spokes/user.py
index beaeae9..aeb5a7a 100644
--- a/pyanaconda/ui/gui/spokes/user.py
+++ b/pyanaconda/ui/gui/spokes/user.py
@@ -19,14 +19,17 @@
 # Red Hat Author(s): Martin Sivak <msivak at redhat.com>
 #
 
+import re
+
 from pyanaconda.i18n import _, CN_
 from pyanaconda.users import cryptPassword, validatePassword, guess_username
 
 from pyanaconda.ui.gui.spokes import NormalSpoke
-from pyanaconda.ui.gui import GUIObject, GUIDialog, check_re, GUICheck
+from pyanaconda.ui.gui import GUIObject
 from pyanaconda.ui.gui.categories.user_settings import UserSettingsCategory
 from pyanaconda.ui.common import FirstbootSpokeMixIn
 from pyanaconda.ui.gui.utils import enlightbox
+from pyanaconda.ui.helpers import GUISpokeInputCheckHandler, GUIInputCheckHandler, InputCheck
 
 from pykickstart.constants import FIRSTBOOT_RECONFIG
 from pyanaconda.constants import ANACONDA_ENVIRON, FIRSTBOOT_ENVIRON,\
@@ -37,31 +40,32 @@ from pyanaconda.regexes import GECOS_VALID, USERNAME_VALID, GROUPNAME_VALID, GRO
 
 __all__ = ["UserSpoke", "AdvancedUserDialog"]
 
-def _checkUsername(editable, data):
-    """Validate a username. Allow empty usernames."""
-    if not (editable.get_text()):
-        return GUICheck.CHECK_OK
-    else:
-        return check_re(editable, data)
-
-def _validateGroups(editable, data):
-    groups_list = editable.get_text().split(",")
-
-    # Check each group name in the list
-    for group in groups_list:
-        group_name = GROUPLIST_FANCY_PARSE.match(group).group('name')
-        if not GROUPNAME_VALID.match(group_name):
-            return _("Invalid group name: %s") % group_name
-
-    return GUICheck.CHECK_OK
-
-class AdvancedUserDialog(GUIDialog):
+class AdvancedUserDialog(GUIObject, GUIInputCheckHandler):
     builderObjects = ["advancedUserDialog", "uid", "gid"]
     mainWidgetName = "advancedUserDialog"
     uiFile = "spokes/advanced_user.glade"
 
+    def set_status(self, inputcheck):
+        # Set or clear the groups error label based on the check status
+        if inputcheck.check_status == InputCheck.CHECK_OK:
+            self._groupsError.set_text('')
+        else:
+            self._groupsError.set_text(inputcheck.check_status)
+
+    def _validateGroups(self, inputcheck):
+        groups_list = self.get_input(inputcheck.input_obj).split(",")
+
+        # Check each group name in the list
+        for group in groups_list:
+            group_name = GROUPLIST_FANCY_PARSE.match(group).group('name')
+            if not GROUPNAME_VALID.match(group_name):
+                return _("Invalid group name: %s") % group_name
+
+        return InputCheck.CHECK_OK
+
     def __init__(self, user, groupDict, data):
-        GUIDialog.__init__(self, data)
+        GUIObject.__init__(self, data)
+        GUIInputCheckHandler.__init__(self)
         self._user = user
         self._groupDict = groupDict
 
@@ -85,9 +89,7 @@ class AdvancedUserDialog(GUIDialog):
         self._grabObjects()
 
         # Validate the group input box
-        self.add_check_with_error_label(editable=self._tGroups,
-                error_label=self._groupsError,
-                run_check=_validateGroups)
+        self.add_check(self._tGroups, self._validateGroups)
 
     def _apply_checkboxes(self, _editable = None, data = None):
         """Update the state of this screen according to the
@@ -184,7 +186,7 @@ class AdvancedUserDialog(GUIDialog):
 
         return rc
 
-class UserSpoke(FirstbootSpokeMixIn, NormalSpoke):
+class UserSpoke(FirstbootSpokeMixIn, NormalSpoke, GUISpokeInputCheckHandler):
     builderObjects = ["userCreationWindow"]
 
     mainWidgetName = "userCreationWindow"
@@ -214,6 +216,7 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke):
 
     def __init__(self, *args):
         NormalSpoke.__init__(self, *args)
+        GUISpokeInputCheckHandler.__init__(self)
         self._oldweak = None
 
     def initialize(self):
@@ -290,8 +293,8 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke):
         self.add_check(self.confirm, self._checkPasswordEmpty)
 
         # Allow empty usernames so the spoke can be exited without creating a user
-        self.add_check(self.username, _checkUsername,
-                {'regex': USERNAME_VALID, 'message': _("Invalid username")})
+        self.add_re_check(self.username, re.compile(USERNAME_VALID.pattern + r'|^$'),
+                _("Invalid username"))
 
         self.add_re_check(self.fullname, GECOS_VALID, _("Full name cannot contain colon characters"))
 
@@ -302,7 +305,7 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke):
     def refresh(self):
         # Enable the input checks in case they were disabled on the last exit
         for check in self.checks:
-            check.enable()
+            check.enabled = True
 
         self.username.set_text(self._user.name)
         self.fullname.set_text(self._user.gecos)
@@ -461,7 +464,7 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke):
             self.username.set_text(username)
             self.guesser[self.username] = True
 
-    def _checkPasswordEmpty(self, editable, data):
+    def _checkPasswordEmpty(self, inputcheck):
         """Check whether a password has been specified at all.
 
            This check is used for both the password and the confirmation.
@@ -469,46 +472,43 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke):
 
         # If the password was set by kickstart, skip the strength check
         if self._user.password_kickstarted:
-            return GUICheck.CHECK_OK
+            return InputCheck.CHECK_OK
 
         # Skip the check if no password is required
         if (not self.usepassword.get_active()) or self._user.password_kickstarted:
-            return GUICheck.CHECK_OK
-        elif not editable.get_text():
-            if editable == self.pw:
+            return InputCheck.CHECK_OK
+        elif not self.get_input(inputcheck.input_obj):
+            if inputcheck.input_obj == self.pw:
                 return _(PASSWORD_EMPTY_ERROR)
             else:
                 return _(PASSWORD_CONFIRM_ERROR_GUI)
         else:
-            return GUICheck.CHECK_OK
+            return InputCheck.CHECK_OK
 
-    def _checkPasswordConfirm(self, editable=None, reset_status=None):
+    def _checkPasswordConfirm(self, inputcheck):
         """If the user has entered confirmation data, check whether it matches the password."""
 
-        # This check is triggered by changes to either the password field or the
-        # confirmation field. If this method is being run from a successful check
-        # to reset the status, just return success
-        if reset_status:
-            return GUICheck.CHECK_OK
-
         # Skip the check if no password is required
         if (not self.usepassword.get_active()) or self._user.password_kickstarted:
-            result = GUICheck.CHECK_OK
+            result = InputCheck.CHECK_OK
         elif self.confirm.get_text() and (self.pw.get_text() != self.confirm.get_text()):
             result = _(PASSWORD_CONFIRM_ERROR_GUI)
         else:
-            result = GUICheck.CHECK_OK
+            result = InputCheck.CHECK_OK
 
         # If the check succeeded, reset the status of the other check object
-        if result == GUICheck.CHECK_OK:
-            if editable == self.confirm:
-                self._password_check.update_check_status(check_data=True)
+        # Disable the current check to prevent a cycle
+        inputcheck.enabled = False
+        if result == InputCheck.CHECK_OK:
+            if inputcheck == self._confirm_check:
+                self._password_check.update_check_status()
             else:
-                self._confirm_check.update_check_status(check_data=True)
+                self._confirm_check.update_check_status()
+        inputcheck.enabled = True
 
         return result
 
-    def _checkPasswordStrength(self, editable=None, data=None):
+    def _checkPasswordStrength(self, inputcheck):
         """Update the error message based on password strength.
 
            The password strength has already been checked in _updatePwQuality, called
@@ -523,7 +523,7 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke):
         # Skip the check if no password is required
         if (not self.usepassword.get_active()) or \
                 ((not self.pw.get_text()) and (self._user.password_kickstarted)):
-            return GUICheck.CHECK_OK
+            return InputCheck.CHECK_OK
 
         # If the password failed the validity check, fail this check
         if (not self._pwq_valid) and (self._pwq_error):
@@ -534,7 +534,7 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke):
         if pwstrength < 2:
             # If Done has been clicked twice, waive the check
             if self._waivePasswordClicks > 1:
-                return GUICheck.CHECK_OK
+                return InputCheck.CHECK_OK
             elif self._waivePasswordClicks == 1:
                 if self._pwq_error:
                     return _(PASSWORD_WEAK_CONFIRM_WITH_ERROR) % self._pwq_error
@@ -546,7 +546,7 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke):
                 else:
                     return _(PASSWORD_WEAK)
         else:
-            return GUICheck.CHECK_OK
+            return InputCheck.CHECK_OK
 
     def on_advanced_clicked(self, _button, data=None):
         """Handler for the Advanced.. button. It starts the Advanced dialog
@@ -577,5 +577,7 @@ class UserSpoke(FirstbootSpokeMixIn, NormalSpoke):
         if not self.username.get_text():
             for check in self.checks:
                 check.disable()
-        NormalSpoke.on_back_clicked(self, button)
+
+        if GUISpokeInputCheckHandler.on_back_clicked(self, button):
+            NormalSpoke.on_back_clicked(self, button)
 
diff --git a/pyanaconda/ui/helpers.py b/pyanaconda/ui/helpers.py
index b517412..eee4d40 100644
--- a/pyanaconda/ui/helpers.py
+++ b/pyanaconda/ui/helpers.py
@@ -55,7 +55,7 @@
 # Mixin.data, so UIObject.data satisfies the requirment that Mixin.data be
 # overriden.
 
-from abc import ABCMeta, abstractproperty
+from abc import ABCMeta, abstractproperty, abstractmethod
 
 from pyanaconda import constants
 from pyanaconda.threads import threadMgr, AnacondaThread
@@ -188,3 +188,235 @@ class SourceSwitchHandler(object):
         self._clean_hdd_iso()
 
         self.data.method.method = None
+
+class InputCheck(object):
+    """Handle an input validation check.
+
+       This class is used by classes that implement InputCheckHandler to
+       manage and manipulate input validation check instances.
+    """
+
+    # Use as a return value to indicate a passed check
+    CHECK_OK = None
+
+    # Read-only properties
+    input_obj = property(lambda s: s._input_obj,
+                     doc="The input to check.")
+    run_check = property(lambda s: s._run_check,
+                         doc="A function to call to perform the input check.")
+    data = property(lambda s: s._data,
+                    doc="Optional data associated with the input check.")
+    set_status = property(lambda s: s._set_status,
+                          doc="A function called when the status changes.")
+    check_status = property(lambda s: s._check_status,
+                            doc="The current status of the check")
+
+    def __init__(self, parent, input_obj, run_check, data=None):
+        """Create a new input validation check.
+
+           :param InputCheckHandler parent: The InputCheckHandler object to which this
+                                            check is being added.
+
+           :param function input_obj: An object representing the input to check.
+
+           :param function run_check: A function to call to perform the input check. This
+                                      function is called with the InputCheck object as a
+                                      parameter.  The return value an object representing
+                                      the error state, or CHECK_OK if the check succeeds.
+
+           :param data: Optional data associated with the input check
+        """
+        self._parent = parent
+        self._input_obj = input_obj
+        self._run_check = run_check
+        self._data = data
+        self._check_status = None
+        self._enabled = True
+
+    def update_check_status(self):
+        """Run an input validation check."""
+        if not self.enabled:
+            return
+
+        new_check_status = self._run_check(self)
+        check_status_changed = (self.check_status != new_check_status)
+        self._check_status = new_check_status
+
+        if check_status_changed:
+            self._parent.set_status(self)
+
+    @property
+    def enabled(self):
+        return self._enabled
+
+    @enabled.setter
+    def enabled(self, value):
+        self._enabled = value
+
+        # If disabling the check, clear the status
+        if not value:
+            self._check_status = None
+
+class InputCheckHandler(object):
+    """Provide a framework for adding input validation checks to a screen.
+
+       This helper class provides a mean of defining and associating input
+       validation checks with an input screen. Running the checks and acting
+       upon the results is left up to the subclasses. Classes implementing
+       InputCheckHandler should ensure that the checks are run at the
+       appropriate times (e.g., calling InputCheck.update_check_status when
+       input is changed), and that input for the screen is not accepted if
+       self.failed_checks is not empty.
+
+       See GUIInputCheckHandler and GUISpokeInputCheckHandler for additional
+       functionality.
+    """
+
+    __metaclass__ = ABCMeta
+
+    def __init__(self):
+        self._check_list = []
+
+    def _check_re(self, inputcheck):
+        """Perform an input validation check against a regular expression."""
+        if inputcheck.data['regex'].match(self.get_input(inputcheck.input_obj)):
+            return inputcheck.CHECK_OK
+        else:
+            return inputcheck.data['message']
+
+    @abstractmethod
+    def get_input(self, input_obj):
+        """Return the input string from an input object.
+
+           :param input_obj: The input object
+
+           :returns: An input string
+           :rtype: str
+        """
+        pass
+
+    @abstractmethod
+    def set_status(self, inputcheck):
+        """Update the status of the window from the input validation results.
+
+           This function could, for example, set or clear an error on the window,
+           or display a message near an input area with invalid data.
+
+           :param InputCheck inputcheck: The InputCheck object whose status last changed.
+        """
+        pass
+
+    def add_check(self, input_obj, run_check, data=None):
+
+        """Add an input validation check to this object.
+
+           :param input_obj: An object representing the input to check.
+
+           :param function run_check: A function to call to perform the input check. This
+                                      function is called with the InputCheck object as a
+                                      parameter.  The return value an object representing
+                                      the error state, or CHECK_OK if the check succeeds.
+
+           :param data: Optional data associated with the input check
+
+           :returns: The InputCheck object created.
+           :rtype: InputCheck
+        """
+        checkRef = InputCheck(self, input_obj, run_check, data)
+        self._check_list.append(checkRef)
+        return checkRef
+
+    def add_re_check(self, input_obj, regex, message):
+        """Add a check using a regular expression.
+
+           :param function input_obj: An object representing the input to check.
+
+           :param re.RegexObject regex: The regular expression to check input against.
+
+           :param str message: A message to return for failed checks
+
+           :returns: The InputCheck object created.
+           :rtype: InputCheck
+        """
+        return self.add_check(input_obj=input_obj, run_check=self._check_re,
+                data={'regex': regex, 'message': message})
+
+    @property
+    def failed_checks(self):
+        """A generator of all failed input checks"""
+        return (c for c in self._check_list if c.check_status != InputCheck.CHECK_OK)
+
+    @property
+    def checks(self):
+        """An iterator over all input checks"""
+        return self._check_list.__iter__()
+
+# pylint: disable-msg=W0223
+class GUIInputCheckHandler(InputCheckHandler):
+    """Provide InputCheckHandler functionality for Gtk input screens.
+
+       This class assumes that all input objects are of type GtkEditable and
+       attaches InputCheck.update_check_status to the changed signal.
+    """
+
+    __metaclass__ = ABCMeta
+
+    def _update_check_status(self, editable, inputcheck):
+        inputcheck.update_check_status()
+
+    def get_input(self, input_obj):
+        return input_obj.get_text()
+
+    def add_check(self, input_obj, run_check, data=None):
+        checkRef = InputCheckHandler.add_check(self, input_obj, run_check, data)
+        input_obj.connect_after("changed", self._update_check_status, checkRef)
+        return checkRef
+
+class GUISpokeInputCheckHandler(GUIInputCheckHandler):
+    """Provide InputCheckHandler functionality for graphical spokes.
+
+       This class implements set_status to set a message in the warning area of
+       the spoke window and provides an implementation of on_back_clicked to
+       prevent the user from exiting a spoke with bad input.
+    """
+
+    __metaclass__ = ABCMeta
+
+    def set_status(self, inputcheck):
+        """Update the warning with the input validation error from the first
+           failed check.
+        """
+        failed_check = next(self.failed_checks, None)
+
+        self.clear_info()
+        if failed_check:
+            self.set_warning(failed_check.check_status)
+            self.window.show_all()
+
+    @abstractmethod
+    def clear_info(self):
+        pass
+
+    @abstractmethod
+    def set_warning(self, msg):
+        pass
+
+    @abstractproperty
+    def window(self):
+        pass
+
+    @abstractmethod
+    def on_back_clicked(self, window):
+        """Check whether the input validation checks allow the spoke to be exited.
+
+           Unlike NormalSpoke.on_back_clicked, this function returns a boolean value.
+           Classes implementing this class should run GUISpokeInputCheckHandler.on_back_clicked,
+           and if it succeeded, run NormalSpoke.on_back_clicked.
+        """
+        failed_check = next(self.failed_checks, None)
+
+        if failed_check:
+            failed_check.input_obj.grab_focus()
+            return False
+        else:
+            return True
-- 
1.8.5.2



More information about the anaconda-patches mailing list