def test_empty(self): c = ListRowContainer(columns=1) c.render(10) result = c.get_lines() self.assertEqual(len(result), 0)
class ScreenWithListWidget(UIScreen): def __init__(self, widgets_count): super().__init__() self._widgets_count = widgets_count self._list_widget = None self.container_callback_input = -1 def refresh(self, args=None): super().refresh(args) self._list_widget = ListRowContainer(2) for i in range(self._widgets_count): self._list_widget.add(TextWidget("Test %s" % i), self._callback, i) self.window.add(self._list_widget) def input(self, args, key): self.close() if self._list_widget.process_user_input(key): return InputState.PROCESSED return InputState.DISCARDED def _callback(self, data): self.container_callback_input = data
def test_list_callback_without_data(self, out_mock, in_mock): c = ListRowContainer(1) c.add(TextWidget("Test"), self._callback) self.assertTrue(c.process_user_input("1")) self.assertIsNone(self._callback_called)
def refresh(self, args=None): super().refresh(args) self._list_widget = ListRowContainer(2) for i in range(self._widgets_count): self._list_widget.add(TextWidget("Test %s" % i), self._callback, i) self.window.add(self._list_widget)
def test_list_correct_input_processing(self, out_mock, in_mock): c = ListRowContainer(1) self._prepare_callbacks(c, 3) self.assertTrue(c.process_user_input("2")) self.assertEqual(self._callback_called, 2)
def refresh(self, args=None): """This methods fills the self.window list by all the objects we want shown on this screen. Title and Spokes mostly.""" TUIObject.refresh(self, args) self._container = ListRowContainer(2, columns_width=39, spacing=2) for w in self._spokes_map: self._container.add(w, callback=self._item_called, data=w) self.window.add_with_separator(self._container)
def test_more_columns_than_widgets(self): c = ListRowContainer(columns=3, items=[self.w1], columns_width=40, numbering=False) c.render(80) expected_result = [u"Můj krásný dlouhý text"] res_lines = c.get_lines() self.evaluate_result(res_lines, expected_result)
def test_listrow_container(self): c = ListRowContainer(columns=2, items=[self.w2, self.w3, self.w5], columns_width=10, spacing=2, numbering=False) c.render(25) expected_result = [u"Test Test 2", u"Test 3"] res_lines = c.get_lines() self.evaluate_result(res_lines, expected_result)
def test_list_container_too_small(self): # to be able to render this container we need at least 11 width # 8 will take only spacing and then 1 for every column c = ListRowContainer(3, spacing=4, numbering=False) c.add(TextWidget("This can't be rendered.")) c.add( TextWidget("Because spacing takes more space than maximal width.")) c.add(TextWidget("Exception will raise.")) with self.assertRaisesRegex(ValueError, "Columns width is too small."): c.render(10)
def test_row_numbering(self): # spacing is 3 by default c = ListRowContainer(2, [self.w1, self.w2, self.w3, self.w4], columns_width=16) c.render(25) expected_result = [ u"1) Můj krásný 2) Test", u" dlouhý text", u"3) Test 2 4) Krásný dlouhý", u" text podruhé" ] res_lines = c.get_lines() self.evaluate_result(res_lines, expected_result)
def test_listrow_wrapping(self): # spacing is 3 by default c = ListRowContainer(2, [self.w1, self.w2, self.w3, self.w4], columns_width=15, numbering=False) c.render(25) expected_result = [ "Můj krásný Test", "dlouhý text", "Test 2 Krásný dlouhý", " text podruhé" ] res_lines = c.get_lines() self.evaluate_result(res_lines, expected_result)
def test_list_container_too_small_turn_off_numbering(self): # to be able to render this container we need 11 width + three times numbers (3 characters) = 20 # 8 will take only spacing and then 1 for every column c = ListRowContainer(3, spacing=4, numbering=True) c.add(TextWidget("This can't be rendered.")) c.add( TextWidget("Because spacing takes more space than maximal width.")) c.add( TextWidget( "Exception will raise with info to turn off numbering.")) with self.assertRaisesRegex( ValueError, "Increase column width or disable numbering."): c.render(19)
def refresh(self, args=None): NormalTUISpoke.refresh(self, args) self._container = ListRowContainer(1) log.debug("license found") # make the options aligned to the same column (the checkbox has the # '[ ]' prepended) self._container.add( TextWidget("%s\n" % _("Read the License Agreement")), self._show_license_screen_callback) self._container.add( CheckboxWidget(title=_("I accept the license agreement."), completed=self.data.eula.agreed), self._license_accepted_callback) self.window.add_with_separator(self._container)
def refresh(self, args=None): """Refresh method is called always before the screen will be printed. All items for printing should be updated or created here. """ # Init window container. The windows container will be erased here. # The window container is the base container. Everything for rendering should be put # into this container, including other containers. super().refresh(args) # Add the screen header message before our items. header = TextWidget("Please complete all the spokes to continue") header = CenterWidget(header) self.window.add_with_separator(header, blank_lines=2) # Create the empty container. # It will add numbering, process user input and positioning for us. self._container = ListRowContainer(2) # Create widget to get user name. widget = self._create_name_widget() # Add widget, callback, arguments to the container. # # widget - Widget we want to render. It will be numbered automatically. # Could be container if needed. # callback - This callback will be called by the ListRowContainer.process_user_input() # method when a user press the number of this item. Callback will get args # passed as 3rd argument. # args - Argument for callback. self._container.add(widget, self._push_screen_callback, self._name_spoke) # Create surname widget and add it to the container. widget = self._create_surname_widget() self._container.add(widget, self._push_screen_callback, self._surname_spoke) # Create password widget and add it to the container. widget = self._create_password_widget() self._container.add(widget, self._push_screen_callback, self._password_spoke) # Add the ListRowContainer container to the WindowContainer container. self.window.add_with_separator(self._container)
def refresh(self, args=None): """This methods fills the self.window list by all the objects we want shown on this screen. Title and Spokes mostly.""" TUIObject.refresh(self, args) self._container = ListRowContainer(2, columns_width=39, spacing=2) for w in self._spokes_map: self._container.add(w, callback=self._item_called, data=w) self.window.add_with_separator(self._container)
def test_newline_wrapping(self): widgets = [ TextWidget("Hello"), TextWidget("Wrap\nthis\ntext"), TextWidget("Hi"), TextWidget("Hello2") ] c = ListRowContainer(3, widgets, columns_width=6, spacing=1, numbering=False) c.render(80) expected_result = [ u"Hello Wrap Hi", u" this", u" text", u"Hello2" ] res_lines = c.get_lines() self.evaluate_result(res_lines, expected_result)
def test_custom_numbering(self): # spacing is 3 by default c = ListRowContainer(2, [self.w1, self.w2, self.w3, self.w4], columns_width=20) c.key_pattern = KeyPattern("a {:d} a ") c.render(25) expected_result = [ u"a 1 a Můj krásný a 2 a Test", u" dlouhý text", u"a 3 a Test 2 a 4 a Krásný dlouhý", u" text podruhé" ] res_lines = c.get_lines() self.evaluate_result(res_lines, expected_result)
class EULASpoke(FirstbootOnlySpokeMixIn, NormalTUISpoke): """The EULA spoke providing ways to read the license and agree/disagree with it.""" category = LicensingCategory def __init__(self, *args, **kwargs): NormalTUISpoke.__init__(self, *args, **kwargs) self.title = _("License information") self._container = None def initialize(self): NormalTUISpoke.initialize(self) def refresh(self, args=None): NormalTUISpoke.refresh(self, args) self._container = ListRowContainer(1) log.debug("license found") # make the options aligned to the same column (the checkbox has the # '[ ]' prepended) self._container.add( TextWidget("%s\n" % _("Read the License Agreement")), self._show_license_screen_callback) self._container.add( CheckboxWidget(title=_("I accept the license agreement."), completed=self.data.eula.agreed), self._license_accepted_callback) self.window.add_with_separator(self._container) @property def completed(self): # Either there is no EULA available, or user agrees/disagrees with it. return self.data.eula.agreed @property def mandatory(self): # This spoke is always mandatory. return True @property def status(self): return _("License accepted") if self.data.eula.agreed else _( "License not accepted") @classmethod def should_run(cls, environment, data): # the EULA spoke should only run in Initial Setup if eula_available() and FirstbootOnlySpokeMixIn.should_run( environment, data): # don't run if we are in reconfig mode and the EULA has already been accepted if data and data.firstboot.firstboot == FIRSTBOOT_RECONFIG and data.eula.agreed: log.debug( "not running license spoke: reconfig mode & license already accepted" ) return False return True return False def apply(self): # nothing needed here, the agreed field is changed in the input method pass def input(self, args, key): if not self._container.process_user_input(key): return key return InputState.PROCESSED @staticmethod def _show_license_screen_callback(data): # show license log.debug("showing the license") eula_screen = LicenseScreen() ScreenHandler.push_screen(eula_screen) def _license_accepted_callback(self, data): # toggle EULA agreed checkbox by changing ksdata log.debug("license accepted state changed to: %s", self.data.eula.agreed) self.data.eula.agreed = not self.data.eula.agreed self.redraw()
class TUIHub(TUIObject, common.Hub): """Base Hub class implementing the pyanaconda.ui.common.Hub interface. It uses text based categories to look for relevant Spokes and manages all the spokes it finds to have the proper category. :param categories: list all the spoke categories to be displayed in this Hub :type categories: list of strings .. inheritance-diagram:: TUIHub :parts: 3 """ categories = [] def __init__(self, data, storage, payload, instclass): TUIObject.__init__(self, data) common.Hub.__init__(self, storage, payload, instclass) self.title = N_("Default HUB title") self._container = None self._spokes_map = [] # hold all the spokes in the right ordering self._spokes = {} # holds spokes referenced by their class name self._spoke_count = 0 # we want user input self.input_required = True def setup(self, args="anaconda"): TUIObject.setup(self, args) environment = args cats_and_spokes = self._collectCategoriesAndSpokes() categories = cats_and_spokes.keys() for c in sorted(categories, key=lambda i: i.title): hub_spokes = [] for spoke_class in cats_and_spokes[c]: # Do the checks for the spoke and create the spoke if spoke_class.should_run(environment, self.data): spoke = spoke_class(self.data, self.storage, self.payload, self.instclass) if spoke.showable: spoke.initialize() else: log.warning("Spoke %s initialization failure!", spoke.__class__.__name__) del spoke continue if spoke.indirect: continue hub_spokes.append(spoke) # sort created spokes and add them to result structures for spoke in sorted(hub_spokes, key=lambda s: s.title): self._spoke_count += 1 self._spokes_map.append(spoke) self._spokes[spoke.__class__.__name__] = spoke if self._spoke_count: # initialization of all expected spokes has been started, so notify the controller hub_controller = lifecycle.get_controller_by_name(self.__class__.__name__) if hub_controller: hub_controller.all_modules_added() else: log.error("Initialization controller for hub %s expected but missing.", self.__class__.__name__) # only schedule the hub if it has some spokes return self._spoke_count != 0 def refresh(self, args=None): """This methods fills the self.window list by all the objects we want shown on this screen. Title and Spokes mostly.""" TUIObject.refresh(self, args) self._container = ListRowContainer(2, columns_width=39, spacing=2) for w in self._spokes_map: self._container.add(w, callback=self._item_called, data=w) self.window.add_with_separator(self._container) def _item_called(self, data): item = data ScreenHandler.push_screen(item) def input(self, args, key): """Handle user input. Numbers are used to show a spoke, the rest is passed to the higher level for processing.""" if self._container.process_user_input(key): return InputState.PROCESSED else: # If we get a continue, check for unfinished spokes. If unfinished # don't continue # TRANSLATORS: 'c' to continue if key == Prompt.CONTINUE: for spoke in self._spokes.values(): if not spoke.completed and spoke.mandatory: print(_("Please complete all spokes before continuing")) return InputState.DISCARDED # TRANSLATORS: 'h' to help elif key == Prompt.HELP: if self.has_help: help_path = ihelp.get_help_path(self.helpFile, self.instclass, True) ScreenHandler.push_screen_modal(HelpScreen(help_path)) self.redraw() return InputState.PROCESSED return key def prompt(self, args=None): """Show an alternative prompt if the hub contains only one spoke. Otherwise it is not readily apparent that the user needs to press 1 to enter the single visible spoke. :param args: optional argument passed from switch_screen calls :type args: anything :return: returns text to be shown next to the prompt for input or None to skip further input processing :rtype: str|None """ prompt = super(TUIHub, self).prompt(args) if self._spoke_count == 1: prompt.add_option("1", _("to enter the %(spoke_title)s spoke") % {"spoke_title": list(self._spokes.values())[0].title}) if self.has_help: prompt.add_help_option() return prompt
class Hub(UIScreen): def __init__(self): super().__init__("Hub") # Container will be used for spokes positioning. Container is always created in # the refresh() method. self._container = None self._create_spokes() def _create_spokes(self): """Create spokes and use their value.""" # Create name spoke self._name_spoke = SetNameScreen("First name", "John") # Create surname spoke self._surname_spoke = SetNameScreen("Surname", "Doe") # Create the PasswordDialog advanced widget for getting password from a user. self._password_spoke = PasswordDialog() def refresh(self, args=None): """Refresh method is called always before the screen will be printed. All items for printing should be updated or created here. """ # Init window container. The windows container will be erased here. # The window container is the base container. Everything for rendering should be put # into this container, including other containers. super().refresh(args) # Add the screen header message before our items. header = TextWidget("Please complete all the spokes to continue") header = CenterWidget(header) self.window.add_with_separator(header, blank_lines=2) # Create the empty container. # It will add numbering, process user input and positioning for us. self._container = ListRowContainer(2) # Create widget to get user name. widget = self._create_name_widget() # Add widget, callback, arguments to the container. # # widget - Widget we want to render. It will be numbered automatically. # Could be container if needed. # callback - This callback will be called by the ListRowContainer.process_user_input() method # when a user press the number of this item. Callback will get args passed as 3rd argument. # args - Argument for callback. self._container.add(widget, self._push_screen_callback, self._name_spoke) # Create surname widget and add it to the container. widget = self._create_surname_widget() self._container.add(widget, self._push_screen_callback, self._surname_spoke) # Create password widget and add it to the container. widget = self._create_password_widget() self._container.add(widget, self._push_screen_callback, self._password_spoke) # Add the ListRowContainer container to the WindowContainer container. self.window.add_with_separator(self._container) def _create_name_widget(self): """Create name spoke widget. Add the actual value below the spoke name. """ msg = "First name" if self._name_spoke.value: msg += "\n{}".format(self._name_spoke.value) return TextWidget(msg) def _create_surname_widget(self): """Create surname spoke widget. Add the actual value below the spoke name. """ msg = "Surname" if self._surname_spoke.value: msg += "\n{}".format(self._surname_spoke.value) return TextWidget(msg) def _create_password_widget(self): """Create password spoke widget. Add the "Password set" text below the spoke name if set. """ msg = "Password" if self._password_spoke.answer: msg += "\nPassword set." return TextWidget(msg) def input(self, args, key): """Run spokes based on the user choice.""" # Find out if a user pressed number for an existing widget and call the callback attached # to it with arguments passed in the refresh() method. # Return False if the input is not related to the widget. if self._container.process_user_input(key): # Do not process other input if spoke is entered. return InputState.PROCESSED # Block continue ('c') if everything is not set. elif key == Prompt.CONTINUE: if self._name_spoke and self._surname_spoke and self._password_spoke.answer: return key else: # catch 'c' key if not everything set return InputState.DISCARDED else: return key def _push_screen_callback(self, target_screen): """Push target screen as new screen. Target screen is passed in as an argument in the refresh() method. """ ScreenHandler.push_screen(target_screen) def prompt(self, args=None): """Add information to prompt for user.""" prompt = super().prompt(args) # Give user hint that he can press 1, 2 or 3 to enter spokes. prompt.add_option("1,2,3", "to enter spokes") return prompt
def test_add_new_container(self): c = ListRowContainer(columns=2, items=[TextWidget("Ahoj")], columns_width=15, spacing=0, numbering=False) expected_result = [u"Ahoj"] c.render(80) self.evaluate_result(c.get_lines(), expected_result) c.add(TextWidget("Nový widget")) c.add(TextWidget("Hello")) expected_result = [u"Ahoj Nový widget", u"Hello"] c.render(80) self.evaluate_result(c.get_lines(), expected_result)
def test_list_container_without_width(self): column_count = 3 spacing_width = 3 c = ListRowContainer(column_count, spacing=spacing_width, numbering=False) c.add(TextWidget("AAAA")) c.add(TextWidget("BBBB")) c.add(TextWidget("CCCCC")) # this line is too long c.add(TextWidget("DDDD")) expected_col_width = 4 expected_spacing_sum = 2 * spacing_width # three columns so 2 spacing between them render_width = (column_count * expected_col_width) + expected_spacing_sum c.render(render_width) expected_result = [u"AAAA BBBB CCCC", u" C", u"DDDD"] res_lines = c.get_lines() self.evaluate_result(res_lines, expected_result)
def test_list_input_processing_none(self, out_mock, in_mock): c = ListRowContainer(1) self._prepare_callbacks(c, 2) self.assertFalse(c.process_user_input(None))
def test_list_input_processing_negative_number(self, out_mock, in_mock): c = ListRowContainer(1) self._prepare_callbacks(c, 3) self.assertFalse(c.process_user_input("-2"))
class TUIHub(TUIObject, common.Hub): """Base Hub class implementing the pyanaconda.ui.common.Hub interface. It uses text based categories to look for relevant Spokes and manages all the spokes it finds to have the proper category. :param categories: list all the spoke categories to be displayed in this Hub :type categories: list of strings .. inheritance-diagram:: TUIHub :parts: 3 """ categories = [] def __init__(self, data, storage, payload): TUIObject.__init__(self, data) common.Hub.__init__(self, storage, payload) self.title = N_("Default HUB title") self._container = None self._spokes_map = [] # hold all the spokes in the right ordering self._spokes = {} # holds spokes referenced by their class name self._spoke_count = 0 # we want user input self.input_required = True def setup(self, args="anaconda"): TUIObject.setup(self, args) environment = args cats_and_spokes = self._collectCategoriesAndSpokes() categories = cats_and_spokes.keys() # display categories by sort order or class name if their # sort order is the same for c in common.sort_categories(categories): hub_spokes = [] for spoke_class in cats_and_spokes[c]: # Do the checks for the spoke and create the spoke if spoke_class.should_run(environment, self.data): spoke = spoke_class(self.data, self.storage, self.payload) if spoke.showable: spoke.initialize() else: log.warning("Spoke %s initialization failure!", spoke.__class__.__name__) del spoke continue if spoke.indirect: continue hub_spokes.append(spoke) # sort created spokes and add them to result structures for spoke in sorted(hub_spokes, key=lambda s: s.title): self._spoke_count += 1 self._spokes_map.append(spoke) self._spokes[spoke.__class__.__name__] = spoke if self._spoke_count: # initialization of all expected spokes has been started, so notify the controller hub_controller = lifecycle.get_controller_by_name( self.__class__.__name__) if hub_controller: hub_controller.all_modules_added() else: log.error( "Initialization controller for hub %s expected but missing.", self.__class__.__name__) # only schedule the hub if it has some spokes return self._spoke_count != 0 def refresh(self, args=None): """This methods fills the self.window list by all the objects we want shown on this screen. Title and Spokes mostly.""" TUIObject.refresh(self, args) self._container = ListRowContainer(2, columns_width=39, spacing=2) for w in self._spokes_map: self._container.add(w, callback=self._item_called, data=w) self.window.add_with_separator(self._container) def _item_called(self, data): item = data ScreenHandler.push_screen(item) def input(self, args, key): """Handle user input. Numbers are used to show a spoke, the rest is passed to the higher level for processing.""" if self._container.process_user_input(key): return InputState.PROCESSED else: # If we get a continue, check for unfinished spokes. If unfinished # don't continue # TRANSLATORS: 'c' to continue if key == Prompt.CONTINUE: for spoke in self._spokes.values(): if not spoke.completed and spoke.mandatory: print( _("Please complete all spokes before continuing")) return InputState.DISCARDED # TRANSLATORS: 'h' to help elif key == Prompt.HELP: if self.has_help: help_path = get_help_path(self.helpFile, True) ScreenHandler.push_screen_modal(HelpScreen(help_path)) return InputState.PROCESSED_AND_REDRAW return key def prompt(self, args=None): """Show an alternative prompt if the hub contains only one spoke. Otherwise it is not readily apparent that the user needs to press 1 to enter the single visible spoke. :param args: optional argument passed from switch_screen calls :type args: anything :return: returns text to be shown next to the prompt for input or None to skip further input processing :rtype: str|None """ prompt = super().prompt(args) if self._spoke_count == 1: prompt.add_option( "1", _("to enter the %(spoke_title)s spoke") % {"spoke_title": list(self._spokes.values())[0].title}) if self.has_help: prompt.add_help_option() return prompt
def test_list_wrong_input_processing(self, out_mock, in_mock): c = ListRowContainer(1) self._prepare_callbacks(c, 3) self.assertFalse(c.process_user_input("c"))