Source code for cfme.web_ui.expression_editor_widgetastic

# -*- coding: utf-8 -*-
""" The expression editor present in some locations of CFME.

"""
from functools import partial
from selenium.common.exceptions import NoSuchElementException
from cfme.utils.wait import wait_for, TimedOutError
import re
from cfme.utils.pretty import Pretty

from widgetastic_patternfly import Input, BootstrapSelect, Button
from widgetastic.widget import View
from widgetastic_manageiq import Calendar
from widgetastic.utils import VersionPick, Version


[docs]class ExpressionEditor(View, Pretty): """This class enables to embed the expression in a Form. Args: show_func: Function to call to show the expression if there are more of them. """ @View.nested class field_form_view(View): # noqa type = BootstrapSelect("chosen_typ") field = BootstrapSelect("chosen_field") key = BootstrapSelect("chosen_key") value = Input(name="chosen_value") user_input = Input(name="user_input") @View.nested class field_date_form(View): # noqa dropdown_select = BootstrapSelect("chosen_from_1") input_select_date = Calendar(name="miq_date_1_0") input_select_time = BootstrapSelect("miq_time_1_0") @View.nested class count_form_view(View): # noqa type = BootstrapSelect("chosen_typ") count = BootstrapSelect("chosen_count") key = BootstrapSelect("chosen_key") value = Input(name="chosen_value") user_input = Input(name="user_input") @View.nested class tag_form_view(View): # noqa type = BootstrapSelect("chosen_typ") tag = BootstrapSelect("chosen_tag") value = BootstrapSelect("chosen_value") user_input = Input(name="user_input") @View.nested class find_form_view(View): # noqa type = BootstrapSelect("chosen_typ") field = BootstrapSelect("chosen_field") skey = BootstrapSelect("chosen_skey") value = Input(name="chosen_value") check = BootstrapSelect("chosen_check") cfield = BootstrapSelect("chosen_cfield") ckey = BootstrapSelect("chosen_ckey") cvalue = Input(name="chosen_cvalue") @View.nested class registry_form_view(View): # noqa type = BootstrapSelect("chosen_typ") key = Input(name="chosen_regkey") value = Input(name="chosen_regval") operation = BootstrapSelect("chosen_key") contents = Input(name="chosen_value") @View.nested class date_specific_form_view(View): # noqa date = Calendar(name="miq_date_1_0") time = BootstrapSelect("miq_time_1_0") @View.nested class date_relative_form_view(View): # noqa from_ = BootstrapSelect("chosen_from_1") through = BootstrapSelect("chosen_through_1") ROOT = "//div[@id='exp_editor_div']" MAKE_BUTTON = "//span[not(contains(@style,'none'))]//img[@alt='{}']" ATOM_ROOT = "./div[@id='exp_atom_editor_div']" EXPRESSIONS_ROOT = "./fieldset/div" COMMIT = VersionPick({ Version.lowest(): "//img[@alt='Commit expression element changes']", "5.7.1": Button(title="Commit expression element changes"), }) DISCARD = VersionPick({ Version.lowest(): "//img[@alt='Discard expression element changes']", "5.7.1": Button(title="Discard expression element changes"), }) REMOVE = VersionPick({ Version.lowest(): ("//span[not(contains(@style, 'none'))]/" "/img[@alt='Remove this expression element']"), "5.8": Button(title="Remove this expression element"), }) NOT = VersionPick({ Version.lowest(): ("//span[not(contains(@style, 'none'))]" "//img[@alt='Wrap this expression element with a NOT']"), "5.8": Button(title="Wrap this expression element with a NOT"), }) OR = VersionPick({ Version.lowest(): ("//span[not(contains(@style, 'none'))]/" "/img[@alt='OR with a new expression element']"), "5.8": Button(title="OR with a new expression element"), }) AND = VersionPick({ Version.lowest(): ("//span[not(contains(@style, 'none'))]/" "/img[@alt='AND with a new expression element']"), "5.8": Button(title="AND with a new expression element"), }) REDO = VersionPick({ Version.lowest(): "//img[@alt='Redo']", "5.8": Button(title="Redo the last change"), }) UNDO = VersionPick({ Version.lowest(): "//img[@alt='Undo']", "5.8": Button(title="Undo the last change"), }) SELECT_SPECIFIC = "//img[@alt='Click to change to a specific Date/Time format']" SELECT_RELATIVE = "//img[@alt='Click to change to a relative Date/Time format']" pretty_attrs = ['show_loc'] def __init__(self, parent, show_loc=None, logger=None): View.__init__(self, parent, logger=logger) self.show_loc = show_loc def __locator__(self): return self.ROOT
[docs] def click_undo(self): self.browser.click(self.UNDO)
[docs] def click_redo(self): self.browser.click(self.REDO)
[docs] def click_and(self): self.browser.click(self.AND)
[docs] def click_or(self): self.browser.click(self.OR)
[docs] def click_not(self): self.browser.click(self.NOT)
[docs] def click_remove(self): self.browser.click(self.REMOVE)
[docs] def click_commit(self): self.browser.click(self.COMMIT)
[docs] def click_discard(self): self.browser.click(self.DISCARD)
[docs] def click_switch_to_relative(self): self.browser.click(self.SELECT_RELATIVE)
[docs] def click_switch_to_specific(self): self.browser.click(self.SELECT_SPECIFIC)
@property def _atom_root(self): return self.browser.element(self.ATOM_ROOT) @property def _expressions_root(self): return self.browser.element(self.EXPRESSIONS_ROOT)
[docs] def select_first_expression(self): """There is always at least one (???), so no checking of bounds.""" self.browser.elements("//a[contains(@id,'exp_')]", parent=self._expressions_root)[0].click()
[docs] def select_expression_by_text(self, text): self.browser.click( "//a[contains(@id,'exp_')][contains(normalize-space(text()),'{}')]".format(text) )
[docs] def no_expression_present(self): els = self.browser.elements("//a[contains(@id,'exp_')]", parent=self._expressions_root) if len(els) > 1: return False return els[0].text.strip() == "???"
[docs] def any_expression_present(self): return not self.no_expression_present()
[docs] def is_editing(self): try: self.browser.element( "//a[contains(@id,'exp_')][contains(normalize-space(text()),'???')]", parent=self._expressions_root ) return True except NoSuchElementException: return False
[docs] def delete_whole_expression(self): while self.any_expression_present(): self.select_first_expression() self.click_remove()
[docs] def read(self): """Returns whole expression as represented visually.""" return self._expressions_root.text.encode("utf-8").strip()
[docs] def enable_editor(self): try: el = self.browser.element(self.show_loc) wait_for(lambda: el.is_displayed, num_sec=2, delay=0.2) el.click() except (TimedOutError, NoSuchElementException): pass
[docs] def fill(self, expression): if self.show_loc is not None: self.enable_editor() prog = create_program(expression, self) before = self._expressions_root.text.encode("utf-8").strip() prog() after = self._expressions_root.text.encode("utf-8").strip() return before != after
[docs] def fill_count(self, count=None, key=None, value=None): """ Fills the 'Count of' type of form. If the value is unspecified and we are in the advanced search form (user input), the user_input checkbox will be checked if the value is None. Args: count: Name of the field to compare (Host.VMs, ...). key: Operation to do (=, <, >=, ...). value: Value to check against. Returns: See :py:func:`cfme.web_ui.fill`. """ view = self.count_form_view view.fill(dict( type="Count of", count=count, key=key, value=value )) # In case of advanced search box if view.user_input.is_displayed: user_input = value is None view.user_input.fill(user_input) self.click_commit()
[docs] def fill_tag(self, tag=None, value=None): """ Fills the 'Tag' type of form. Args: tag: Name of the field to compare. value: Value to check against. """ view = self.tag_form_view view.fill(dict( type="Tag", tag=tag, value=value )) # In case of advanced search box if view.user_input.is_displayed: user_input = value is None view.user_input.fill(user_input) self.click_commit()
[docs] def fill_registry(self, key=None, value=None, operation=None, contents=None): """ Fills the 'Registry' type of form.""" view = self.registry_form_view view.fill(dict( type="Registry", key=key, value=value, operation=operation, contents=contents, )) self.click_commit()
[docs] def fill_find(self, field=None, skey=None, value=None, check=None, cfield=None, ckey=None, cvalue=None): view = self.find_form_view view.fill(dict( type="Find", field=field, skey=skey, value=value, check=check, cfield=cfield, ckey=ckey, cvalue=cvalue )) self.click_commit()
[docs] def fill_field(self, field=None, key=None, value=None): """ Fills the 'Field' type of form. Args: tag: Name of the field to compare (Host.VMs, ...). key: Operation to do (=, <, >=, IS NULL, ...). value: Value to check against. Returns: See :py:func:`cfme.web_ui.fill`. """ field_norm = field.strip().lower() if ("date updated" in field_norm or "date created" in field_norm or "boot time" in field_norm or "timestamp" in field_norm): no_date = False else: no_date = True view = self.field_form_view view.fill(dict( type="Field", field=field, key=key, value=value if no_date else None, )) # In case of advanced search box if view.user_input.is_displayed: user_input = value is None view.user_input.fill(user_input) if not no_date: # Flip the right part of form view = self.field_date_form if (isinstance(value, basestring) and not re.match(r"^[0-9]{2}/[0-9]{2}/[0-9]{4}$", value)): if not view.dropdown_select.is_displayed: self.click_switch_to_relative() view.fill({"dropdown_select": value}) self.click_commit() else: # Specific selection if not view.input_select_date.is_displayed: self.click_switch_to_specific() if (isinstance(value, tuple) or isinstance(value, list)) and len(value) == 2: date, time = value elif isinstance(value, basestring): # is in correct format mm/dd/yyyy # Date only (for now) date = value[:] time = None else: raise TypeError("fill_field expects a 2-tuple (date, time) or string with date") # TODO datetime.datetime support view.input_select_date.fill(date) # Try waiting a little bit for time field # If we don't wait, committing the expression will glitch try: wait_for(lambda: view.input_select_time.is_displayed, num_sec=6) # It appeared, so if the time is to be set, we will set it # (passing None glitches) if time: view.input_select_time.fill(time) except TimedOutError: # Did not appear, ignore that pass finally: # And finally, commit the expression :) self.click_commit() else: self.click_commit()
[docs]def get_func(name, context): """ Return callable from this module by its name. Args: name: Name of the variable containing the callable. Returns: Callable from this module """ assert not name.startswith("_"), "Command '{}' is private!".format(name) try: func = getattr(context, name) except AttributeError: raise NameError("Could not find function {} to operate the editor!".format(name)) try: func.__call__ return func except AttributeError: raise NameError("{} is not callable!".format(name))
[docs]def run_commands(command_list, clear_expression=True, context=None): """ Run commands from the command list. Command list syntax: .. code-block:: python [ "function1", # no args "function2", # dtto {"fill_fields": {"field1": "value", "field2": "value"}}, # Passes kwargs {"do_other_things": [1,2,3]} # Passes args ] In YAML: .. code-block:: yaml - function1 - function2 - fill_fields: field1: value field2: value - do_other_things: - 1 - 2 - 3 Args: command_list: :py:class:`list` object of the commands clear_expression: Whether to clear the expression before entering new one (default `True`) context: widget object """ assert isinstance(command_list, list) or isinstance(command_list, tuple) step_list = [] for command in command_list: if isinstance(command, basestring): # Single command, no params step_list.append(get_func(command, context)) elif isinstance(command, dict): for key, value in command.iteritems(): func = get_func(key, context) args = [] kwargs = {} if isinstance(value, list) or isinstance(value, tuple): args.extend(value) elif isinstance(value, dict): kwargs.update(value) else: raise Exception("I use '{}' type here!".format(type(value).__name__)) step_list.append(partial(func, *args, **kwargs)) else: raise Exception("I cannot process '{}' type here!".format(type(command).__name__)) if clear_expression: context.delete_whole_expression() for step in step_list: step()
[docs]def create_program(dsl_program, widget_object): """ Simple DSL to fill the expression editor. Syntax: DSL consists from statements. Statements are separated with newline or ;. Each statement is a single function call. Functions are called in this module. Function without parameters can be called like this: function or function() If the function has some parameters, you have to choose whether they are kwargs or args. DSL has no string literals, so if you want to call a function with classic parameters: function(parameter one, parameter two, you cannot use comma) And with kwargs: function(username=John Doe, password=top secret) You cannot split the statement to multiple lines as the DSL is regexp-based. Args: dsl_program: Source string with the program. Returns: Callable, which fills the expression. """ SIMPLE_CALL = r"^[a-z_A-Z][a-z_A-Z0-9]*$" # noqa ARGS_CALL = r"^(?P<name>[a-z_A-Z][a-z_A-Z0-9]*)\((?P<args>.*)\)$" # noqa KWARG = r"^[^=]+=.*$" # noqa command_list = [] for i, line in enumerate([x.strip() for x in re.split(r"\n|;", dsl_program)]): if len(line) == 0: continue elif re.match(SIMPLE_CALL, line): command_list.append(line) continue args_match = re.match(ARGS_CALL, line) if not args_match: raise SyntaxError("Could not resolve statement `{}' on line {}".format(line, i)) fname = args_match.groupdict()["name"] args = [x.strip() for x in args_match.groupdict()["args"].split(",")] if len(args) > 0 and len(args[0]) > 0: if re.match(KWARG, args[0]): # kwargs kwargs = dict([map(lambda x: x.strip(), x.split("=", 1)) for x in args]) command_list.append({fname: kwargs}) else: # Args command_list.append({fname: [None if arg == "/None/" else arg for arg in args]}) else: command_list.append(fname) return create_program_from_list(command_list, widget_object)
[docs]def create_program_from_list(command_list, widget_object): """ Create function which fills the expression from the command list. Args: command_list: Command list for :py:func:`run_program` Returns: Callable, which fills the expression. """ return partial(run_commands, command_list, context=widget_object)