--- pyanaconda/iutil.py | 36 +++++++ pyanaconda/ui/tui/spokes/__init__.py | 190 +++++++++++++++++++++++++++++++++-- 2 files changed, 220 insertions(+), 6 deletions(-)
diff --git a/pyanaconda/iutil.py b/pyanaconda/iutil.py index 30dbb84..d69b081 100644 --- a/pyanaconda/iutil.py +++ b/pyanaconda/iutil.py @@ -481,6 +481,42 @@ class ProxyString(object): def __str__(self): return self.url
+def getdeepattr(obj, name): + """This behaves as the standard getattr, but supports + composite (containing dots) attribute names. + + As an example: + + >>> import os + >>> from os.path import split + >>> getdeepattr(os, "path.split") == split + True + """ + + for attr in name.split("."): + obj = getattr(obj, attr) + return obj + +def setdeepattr(obj, name, value): + """This behaves as the standard setattr, but supports + composite (containing dots) attribute names. + + As an example: + + >>> class O: + >>> pass + >>> a = O() + >>> a.b = O() + >>> a.b.c = O() + >>> setdeepattr(a, "b.c.d", True) + >>> a.b.c.d + True + """ + path = name.split(".") + for attr in path[:-1]: + obj = getattr(obj, attr) + return setattr(obj, path[-1], value) + def strip_accents(s): """This function takes arbitrary unicode string and returns it with all the diacritics removed. diff --git a/pyanaconda/ui/tui/spokes/__init__.py b/pyanaconda/ui/tui/spokes/__init__.py index 2be2847..cb61eb3 100644 --- a/pyanaconda/ui/tui/spokes/__init__.py +++ b/pyanaconda/ui/tui/spokes/__init__.py @@ -18,15 +18,19 @@ # # Red Hat Author(s): Martin Sivak msivak@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 +from collections import namedtuple +from pyanaconda.iutil import setdeepattr, getdeepattr
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): @@ -80,10 +84,184 @@ 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): + """Spoke/dialog used to read new value of textual or password data""" + + 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, cond)): + 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, cond), key): + if regex.match(key): + self.value = key + self.close() + return True + else: + return NormalTUISpoke.input(self, (title, value, regex), key) + +class OneShotEditTUIDialog(EditTUIDialog): + """The same as EditTUIDialog, but closes automatically after + the value is read + """ + + def prompt(self, (title, value, regex, cond)): + ret = EditTUIDialog.prompt(self, (title, value, regex, cond)) + if ret is None: + self.close() + return ret + +EditTUISpokeEntry = namedtuple("EditTUISpokeEntry", ["title", "attribute", "aux", "visible"]) + +class EditTUISpoke(NormalTUISpoke): + """Spoke with declarative semantics, it contains + a list of titles, attribute names and regexps + that specify the fields of an object the user + allowed to edit. + """ + + # self.data's subattribute name + # empty string means __init__ will provide + # something else + edit_data = "" + + # constants to be used in the aux field + # and mark the entry as a password or checkbox field + PASSWORD = EditTUIDialog.PASSWORD + CHECK = "check" + + # list of fields in the format of named tuples like: + # EditTUISpokeEntry(title, attribute, aux, visible) + # title - Nontranslated title of the entry + # attribute - The edited object's attribute name + # aux - Compiled regular expression or one of the + # two constants from above. + # It will be used to check the value typed + # by user and to show the proper entry + # for password, text or checkbox. + # visible - True, False or a function that accepts + # two arguments - self and the edited object + # It is evaluated and used to display or + # hide this attribute's entry + 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 should hold the object this Spoke is supposed + # to edit + 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, entry): + number = tui.TextWidget("%2d)" % i) + title = tui.TextWidget(_(entry.title)) + value = getdeepattr(self.args, entry.attribute) + value = tui.TextWidget(value) + + return tui.ColumnWidget([(3, [number]), (None, [title, value])], 1) + + def _prep_check(i, entry): + number = tui.TextWidget("%2d)" % i) + value = getdeepattr(self.args, entry.attribute) + ch = tui.CheckboxWidget(title=_(entry.title), completed=bool(value)) + + return tui.ColumnWidget([(3, [number]), (None, [ch])], 1) + + def _prep_password(i, entry): + number = tui.TextWidget("%2d)" % i) + title = tui.TextWidget(_(entry.title)) + value = getdeepattr(self.args, entry.attribute) + value = tui.TextWidget("".join(["*"] * len(value))) + + return tui.ColumnWidget([(3, [number]), (None, [title, value])], 1) + + for idx,entry in enumerate(self.edit_fields): + if callable(entry.visible) and not entry.visible(self, self.args): + continue + elif not callable(entry.visible) and not entry.visible: + continue + + entry_type = entry.aux + if entry_type == self.PASSWORD: + w = _prep_password(idx+1, entry) + elif entry_type == self.CHECK: + w = _prep_check(idx+1, entry) + else: + w = _prep_text(idx+1, entry) + + 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].aux == self.CHECK: + setdeepattr(self.args, self.edit_fields[idx].attribute, + not getdeepattr(self.args, self.edit_fields[idx][1])) + self.app.redraw() + self.apply() + else: + self.app.switch_screen_modal(self.dialog, self.edit_fields[idx]) + if self.dialog.value is not None: + setdeepattr(self.args, self.edit_fields[idx].attribute, + self.dialog.value) + self.apply() + return True + except ValueError: + pass + + return NormalTUISpoke.input(self, args, key) + +class StandaloneTUISpoke(TUISpoke, StandaloneSpoke): pass
class PersonalizationTUISpoke(TUISpoke, PersonalizationSpoke):