[PATCH 3/4] Create declarative EditTUISpoke and use it for users and passwords

Martin Sivak msivak at redhat.com
Fri Mar 8 13:57:03 UTC 2013


This adds three new classes:

EditTUIDialog - spoke/dialog used to read new value of textual
                or password data

OneShotEditTUIDialog - the same as above, but closes automatically
                       after the value is read

EditTUISpoke - spoke with declarative semantics, it contains
               a list of titles, attribute names and regexps
               that specify the fields of an object the user
               is allowed to edit

This patchset also adds the UserSpoke and modifies the Password
spoke to use the implemented helper classes.
---
 pyanaconda/ui/tui/__init__.py        |  83 ++-----------------
 pyanaconda/ui/tui/spokes/__init__.py | 152 +++++++++++++++++++++++++++++++++--
 pyanaconda/ui/tui/spokes/password.py |  35 +++-----
 pyanaconda/ui/tui/spokes/user.py     |  70 ++++++++++++++++
 pyanaconda/ui/tui/tuiobject.py       |  78 ++++++++++++++++++
 5 files changed, 309 insertions(+), 109 deletions(-)
 create mode 100644 pyanaconda/ui/tui/spokes/user.py

diff --git a/pyanaconda/ui/tui/__init__.py b/pyanaconda/ui/tui/__init__.py
index d051590..879f28e 100644
--- a/pyanaconda/ui/tui/__init__.py
+++ b/pyanaconda/ui/tui/__init__.py
@@ -21,12 +21,12 @@
 
 from pyanaconda import ui
 from pyanaconda.ui import common
-from pyanaconda.ui import communication
 from pyanaconda.flags import flags
 import simpleline as tui
 from hubs.summary import SummaryHub
 from hubs.progress import ProgressHub
 from spokes import StandaloneSpoke
+from tuiobject import YesNoDialog, ErrorDialog
 
 import os
 import site
@@ -35,82 +35,6 @@ import meh.ui.text
 import gettext
 _ = lambda x: gettext.ldgettext("anaconda", x)
 
-class ErrorDialog(tui.UIScreen):
-    """Dialog screen for reporting errors to user."""
-
-    title = _("Error")
-
-    def __init__(self, app, message):
-        """
-        :param app: the running application reference
-        :type app: instance of App class
-
-        :param message: the message to show to the user
-        :type message: unicode
-        """
-
-        tui.UIScreen.__init__(self, app)
-        self._message = message
-
-    def refresh(self, args = None):
-        tui.UIScreen.refresh(self, args)
-        text = tui.TextWidget(self._message)
-        self._window.append(tui.CenterWidget(text))
-
-    def prompt(self, args = None):
-        return _("Press enter to exit.")
-
-    def input(self, args, key):
-        """This dialog is closed by any input."""
-        self.close()
-
-class YesNoDialog(tui.UIScreen):
-    """Dialog screen for Yes - No questions."""
-
-    title = _("Question")
-
-    def __init__(self, app, message):
-        """
-        :param app: the running application reference
-        :type app: instance of App class
-
-        :param message: the message to show to the user
-        :type message: unicode
-        """
-
-        tui.UIScreen.__init__(self, app)
-        self._message = message
-        self._response = None
-
-    def refresh(self, args = None):
-        tui.UIScreen.refresh(self, args)
-        text = tui.TextWidget(self._message)
-        self._window.append(tui.CenterWidget(text))
-        self._window.append(u"")
-        return True
-
-    def prompt(self, args):
-        return _("Please respond 'yes' or 'no': ")
-
-    def input(self, args, key):
-        if key == _("yes"):
-            self._response = True
-            self.close()
-            return None
-
-        elif key == _("no"):
-            self._response = False
-            self.close()
-            return None
-
-        else:
-            return False
-
-    @property
-    def answer(self):
-        """The response can be True (yes), False (no) or None (no response)."""
-        return self._response
-
 class TextUserInterface(ui.UserInterface):
     """This is the main class for Text user interface."""
 
@@ -183,6 +107,11 @@ class TextUserInterface(ui.UserInterface):
         """
         self._app = tui.App(self.productTitle, yes_or_no_question = YesNoDialog,
                             quit_message = self.quitMessage)
+
+        import pdb
+        from pyanaconda.ui.communication import HUB_CODE_EXCEPTION, HUB_CODE_INPUT
+        self._app.register_event_handler(HUB_CODE_EXCEPTION, lambda e,x: pdb.set_trace(), None)
+
         _hubs = self._list_hubs()
 
         # First, grab a list of all the standalone spokes.
diff --git a/pyanaconda/ui/tui/spokes/__init__.py b/pyanaconda/ui/tui/spokes/__init__.py
index e4d7d33..6bac732 100644
--- a/pyanaconda/ui/tui/spokes/__init__.py
+++ b/pyanaconda/ui/tui/spokes/__init__.py
@@ -18,15 +18,17 @@
 #
 # Red Hat Author(s): Martin Sivak <msivak at redhat.com>
 #
-from .. import simpleline as tui
-from pyanaconda.ui.tui.tuiobject import TUIObject
+from pyanaconda.ui.tui import simpleline as tui
+from pyanaconda.ui.tui.tuiobject import TUIObject, YesNoDialog
 from pyanaconda.ui.common import Spoke, StandaloneSpoke, NormalSpoke, PersonalizationSpoke, collect
-import os
+from pyanaconda.users import validatePassword
+from pwquality import PWQError
+import re
 
 import gettext
 _ = lambda x: gettext.ldgettext("anaconda", x)
 
-__all__ = ["TUISpoke", "StandaloneSpoke", "NormalSpoke", "PersonalizationSpoke",
+__all__ = ["TUISpoke", "EditTUISpoke", "EditTUIDialog", "StandaloneSpoke", "NormalSpoke", "PersonalizationSpoke",
            "collect_spokes", "collect_categories"]
 
 class TUISpoke(TUIObject, tui.Widget, Spoke):
@@ -72,10 +74,148 @@ class TUISpoke(TUIObject, tui.Widget, Spoke):
         c.render(width)
         self.draw(c)
 
-class StandaloneTUISpoke(TUISpoke, StandaloneSpoke):
+class NormalTUISpoke(TUISpoke, NormalSpoke):
     pass
 
-class NormalTUISpoke(TUISpoke, NormalSpoke):
+class EditTUIDialog(NormalTUISpoke):
+    title = _("New value")
+    PASSWORD = re.compile(".*")
+
+    def __init__(self, app, data, storage, payload, instclass):
+        NormalTUISpoke.__init__(self, app, data, storage, payload, instclass)
+        self.value = None
+
+    def refresh(self, args):
+        self._window = []
+        self.value = None
+        return True
+
+    def prompt(self, (title, value, regex)):
+        if regex == self.PASSWORD:
+            pw = self._app.raw_input(_("%s: ") % title, hidden=True)
+            confirm = self._app.raw_input(_("%s (confirm): ") % title, hidden=True)
+            error = None
+            # just returning an error is either blank or mismatched
+            # passwords.  Raising is because of poor quality.
+            try:
+                error = validatePassword(pw, confirm)
+                if error:
+                    print(error)
+                    return None
+            except PWQError as (e, msg):
+                error = _("You have provided a weak password: %s. " % msg)
+                error += _("\nWould you like to use it anyway?")
+                question_window = YesNoDialog(self._app, error)
+                self._app.switch_screen_modal(question_window)
+                if not question_window.answer:
+                    return None
+
+            self.value = pw
+            return None
+        else:
+            return _("Enter new value for '%s' and press enter\n") % title
+
+    def input(self, (title, value, regex), key):
+        if regex.match(key):
+            self.value = key
+            self.close()
+            return True
+        else:
+            return NormalTUISpoke.input(self, (title, value, regex), key)
+
+class OneShotEditTUIDialog(EditTUIDialog):
+    def prompt(self, (title, value, regex)):
+        ret = EditTUIDialog.prompt(self, (title, value, regex))
+        if ret is None:
+            self.close()
+        return ret
+
+class EditTUISpoke(NormalTUISpoke):
+    # self.data's subattribute name
+    edit_data = ""
+
+    PASSWORD = EditTUIDialog.PASSWORD
+    CHECK = "check"
+
+    # list of fields in the format (title, attribute, compiled_regexp)
+    edit_fields = [
+    ]
+
+    def __init__(self, app, data, storage, payload, instclass):
+        NormalTUISpoke.__init__(self, app, data, storage, payload, instclass)
+        self.dialog = OneShotEditTUIDialog(app, data, storage, payload, instclass)
+        self.args = None
+
+    def refresh(self, args = None):
+        NormalTUISpoke.refresh(self, args)
+
+        if args:
+            self.args = args
+        elif self.edit_data:
+            self.args = self.data
+            for key in self.edit_data.split("."):
+                self.args = getattr(self.args, key)
+
+        def _prep_text(i, (title, field, regexp)):
+            number = tui.TextWidget("%2d)" % i)
+            title = tui.TextWidget(title)
+            value = self.args
+            for key in field.split("."):
+                value = getattr(self.args, key)
+            value = tui.TextWidget(value)
+
+            return tui.ColumnWidget([(3, [number]), (None, [title, value])], 1)
+
+        def _prep_check(i, (title, field, regexp)):
+            number = tui.TextWidget("%2d)" % i)
+            value = self.args
+            for key in field.split("."):
+                value = getattr(self.args, key)
+            ch = tui.CheckboxWidget(title=title, completed=bool(value))
+
+            return tui.ColumnWidget([(3, [number]), (None, [ch])], 1)
+
+        def _prep_password(i, (title, field, regexp)):
+            number = tui.TextWidget("%2d)" % i)
+            title = tui.TextWidget(title)
+            value = self.args
+            for key in field.split("."):
+                value = getattr(self.args, key)
+            value = tui.TextWidget("".join(["*"] * len(value)))
+
+            return tui.ColumnWidget([(3, [number]), (None, [title, value])], 1)
+
+        for idx,field in enumerate(self.edit_fields):
+            field_type = field[2]
+            if field_type == self.PASSWORD:
+                w = _prep_password(idx+1, field)
+            elif field_type == self.CHECK:
+                w = _prep_check(idx+1, field)
+            else:
+                w = _prep_text(idx+1, field)
+
+            self._window.append(w)
+
+        return True
+
+    def input(self, args, key):
+        try:
+            idx = int(key) - 1
+            if idx >= 0 and idx < len(self.edit_fields):
+                if self.edit_fields[idx][2] == self.CHECK:
+                    setattr(self.args, self.edit_fields[idx][1], not getattr(self.args, self.edit_fields[idx][1]))
+                    self.app.redraw()
+                else:
+                    self.app.switch_screen_modal(self.dialog, self.edit_fields[idx])
+                    if self.dialog.value is not None:
+                        setattr(self.args, self.edit_fields[idx][1], self.dialog.value)
+                return True
+        except ValueError:
+            pass
+
+        return NormalTUISpoke.input(self, args, key)
+
+class StandaloneTUISpoke(TUISpoke, StandaloneSpoke):
     pass
 
 class PersonalizationTUISpoke(TUISpoke, PersonalizationSpoke):
diff --git a/pyanaconda/ui/tui/spokes/password.py b/pyanaconda/ui/tui/spokes/password.py
index f6e1745..b28bc3a 100644
--- a/pyanaconda/ui/tui/spokes/password.py
+++ b/pyanaconda/ui/tui/spokes/password.py
@@ -20,22 +20,20 @@
 #                    Jesse Keating <jkeating at redhat.com>
 #
 
-from pyanaconda.ui.tui.spokes import NormalTUISpoke
+from pyanaconda.ui.tui.spokes import EditTUIDialog
+from pyanaconda.ui.common import FirstbootSpokeMixIn
 from pyanaconda.ui.tui.simpleline import TextWidget
-from pyanaconda.ui.tui import YesNoDialog
-from pyanaconda.users import validatePassword
-from pwquality import PWQError
 
 import gettext
 _ = lambda x: gettext.ldgettext("anaconda", x)
 
 
-class PasswordSpoke(NormalTUISpoke):
+class PasswordSpoke(FirstbootSpokeMixIn, EditTUIDialog):
     title = _("Set root password")
     category = "password"
 
     def __init__(self, app, data, storage, payload, instclass):
-        NormalTUISpoke.__init__(self, app, data, storage, payload, instclass)
+        EditTUIDialog.__init__(self, app, data, storage, payload, instclass)
         self._password = None
 
     @property
@@ -58,7 +56,7 @@ class PasswordSpoke(NormalTUISpoke):
             return _("Password is not set.")
 
     def refresh(self, args = None):
-        NormalTUISpoke.refresh(self, args)
+        EditTUIDialog.refresh(self, args)
 
         self._window += [TextWidget(_("Please select new root password. You will have to type it twice.")), ""]
 
@@ -66,26 +64,11 @@ class PasswordSpoke(NormalTUISpoke):
 
     def prompt(self, args = None):
         """Overriden prompt as password typing is special."""
-        pw = self._app.raw_input(_("Password: "), hidden=True)
-        confirm = self._app.raw_input(_("Password (confirm): "), hidden=True)
+        EditTUIDialog.prompt(self, (_("Password"), "", EditTUIDialog.PASSWORD))
+        if self.value == None:
+            return
 
-        error = None
-        # just returning an error is either blank or mismatched
-        # passwords.  Raising is because of poor quality.
-        try:
-            error = validatePassword(pw, confirm)
-            if error:
-                print(error)
-                return None
-        except PWQError as (e, msg):
-            error = _("You have provided a weak password: %s. " % msg)
-            error += _("\nWould you like to use it anyway?")
-            question_window = YesNoDialog(self._app, error)
-            self._app.switch_screen_modal(question_window)
-            if not question_window.answer:
-                return None
-
-        self._password = pw
+        self._password = self.value
         self.apply()
 
         self.close()
diff --git a/pyanaconda/ui/tui/spokes/user.py b/pyanaconda/ui/tui/spokes/user.py
new file mode 100644
index 0000000..f4ee6f8
--- /dev/null
+++ b/pyanaconda/ui/tui/spokes/user.py
@@ -0,0 +1,70 @@
+# Root password text spoke
+#
+# Copyright (C) 2012  Red Hat, Inc.
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
+# Public License for more details.  You should have received a copy of the
+# GNU General Public License along with this program; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
+# source code or documentation are not subject to the GNU General Public
+# License and may only be used or replicated with the express permission of
+# Red Hat, Inc.
+#
+# Red Hat Author(s): Martin Sivak <msivak at redhat.com>
+#
+
+from pyanaconda.ui.tui.spokes import EditTUISpoke
+from pyanaconda.ui.common import FirstbootSpokeMixIn
+from pyanaconda.ui.tui.simpleline import TextWidget
+from pyanaconda.ui.tui import YesNoDialog
+from pyanaconda.users import validatePassword
+
+import re
+import gettext
+_ = lambda x: gettext.ldgettext("anaconda", x)
+__all__ = ["UserSpoke"]
+
+class UserSpoke(FirstbootSpokeMixIn, EditTUISpoke):
+    title = _("Create user")
+    category = "password"
+
+    edit_fields = [
+        ("Username", "name", re.compile("^[a-z0-9_]+$")),
+        ("Fullname", "gecos", re.compile("^[^:]*$")),
+        ("Password", "password", EditTUISpoke.PASSWORD),
+        ("Administrator", "_admin", EditTUISpoke.CHECK),
+        ("Groups", "_groups", re.compile("^([a-z0-9_]+| +)*$"))
+        ]
+
+    def __init__(self, app, data, storage, payload, instclass):
+        FirstbootSpokeMixIn.__init__(self)
+        EditTUISpoke.__init__(self, app, data, storage, payload, instclass)
+
+        if self.data.user.userList:
+            self.args = self.data.user.userList[0]
+        else:
+            self.args = self.data.UserData()
+
+        self.args._admin = "wheel" in self.args.groups
+        self.args._groups = " ".join(self.args.groups)
+
+    @property
+    def completed(self):
+        return self.data.user.userList
+
+    @property
+    def mandatory(self):
+        return True
+
+    @property
+    def status(self):
+        return _("User %s will be created.") % self.args.name
+
+    def apply(self):
+        pass
diff --git a/pyanaconda/ui/tui/tuiobject.py b/pyanaconda/ui/tui/tuiobject.py
index 42a0847..08b95c0 100644
--- a/pyanaconda/ui/tui/tuiobject.py
+++ b/pyanaconda/ui/tui/tuiobject.py
@@ -21,6 +21,84 @@
 
 from pyanaconda.ui import common
 import simpleline as tui
+import gettext
+_ = lambda x: gettext.ldgettext("anaconda", x)
+
+class ErrorDialog(tui.UIScreen):
+    """Dialog screen for reporting errors to user."""
+
+    title = _("Error")
+
+    def __init__(self, app, message):
+        """
+        :param app: the running application reference
+        :type app: instance of App class
+
+        :param message: the message to show to the user
+        :type message: unicode
+        """
+
+        tui.UIScreen.__init__(self, app)
+        self._message = message
+
+    def refresh(self, args = None):
+        tui.UIScreen.refresh(self, args)
+        text = tui.TextWidget(self._message)
+        self._window.append(tui.CenterWidget(text))
+
+    def prompt(self, args = None):
+        return _("Press enter to exit.")
+
+    def input(self, args, key):
+        """This dialog is closed by any input."""
+        self.close()
+
+class YesNoDialog(tui.UIScreen):
+    """Dialog screen for Yes - No questions."""
+
+    title = _("Question")
+
+    def __init__(self, app, message):
+        """
+        :param app: the running application reference
+        :type app: instance of App class
+
+        :param message: the message to show to the user
+        :type message: unicode
+        """
+
+        tui.UIScreen.__init__(self, app)
+        self._message = message
+        self._response = None
+
+    def refresh(self, args = None):
+        tui.UIScreen.refresh(self, args)
+        text = tui.TextWidget(self._message)
+        self._window.append(tui.CenterWidget(text))
+        self._window.append(u"")
+        return True
+
+    def prompt(self, args):
+        return _("Please respond 'yes' or 'no': ")
+
+    def input(self, args, key):
+        if key == _("yes"):
+            self._response = True
+            self.close()
+            return None
+
+        elif key == _("no"):
+            self._response = False
+            self.close()
+            return None
+
+        else:
+            return False
+
+    @property
+    def answer(self):
+        """The response can be True (yes), False (no) or None (no response)."""
+        return self._response
 
 class TUIObject(tui.UIScreen, common.UIObject):
     """Base class for Anaconda specific TUI screens. Implements the
-- 
1.7.11.7



More information about the anaconda-patches mailing list