# -*- coding: utf-8 -*-
""" The expression editor present in some locations of CFME.
"""
from functools import partial
from selenium.common.exceptions import NoSuchElementException
from multimethods import singledispatch
from cfme.utils.wait import wait_for, TimedOutError
import cfme.fixtures.pytest_selenium as sel
from cfme.web_ui import Anything, Calendar, Form, Input, Region, AngularSelect, fill
import re
import sys
import types
from cfme.utils.pretty import Pretty
def _make_button(title):
return "//span[not(contains(@style,'none'))]//img[@alt='{}']".format(title)
def _root():
return sel.element("//div[@id='exp_editor_div']")
def _atom_root():
return sel.element("./div[@id='exp_atom_editor_div']", root=_root())
def _expressions_root():
return sel.element("./fieldset/div", root=_root())
###
# Buttons container
buttons = Region(
locators=dict(
commit="//button[@title='Commit expression element changes']",
discard="//button[@title='Discard expression element changes']",
remove="//span[@id='exp_buttons_on']//*[@title='Remove this expression element']",
NOT="//span[not(contains(@style, 'none'))]" +
"//img[@alt='Wrap this expression element with a NOT']",
OR="//span[not(contains(@style, 'none'))]//img[@alt='OR with a new expression element']",
AND="//span[not(contains(@style, 'none'))]//img[@alt='AND with a new expression element']",
redo="(//button | //a)[@title='Re-apply the previous change']",
undo="(//button | //a)[@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']",
)
)
###
# Buttons for operationg the expression concatenation
#
[docs]def click_undo():
sel.click(buttons.undo)
[docs]def click_redo():
sel.click(buttons.redo)
[docs]def click_and():
sel.click(buttons.AND)
[docs]def click_or():
sel.click(buttons.OR)
[docs]def click_not():
sel.click(buttons.NOT)
[docs]def click_remove():
sel.click(buttons.remove)
###
# Buttons for operating the atomic expressions
#
[docs]def click_commit():
sel.click(buttons.commit)
[docs]def click_discard():
sel.click(buttons.discard)
###
# Functions for operating the selection of the expressions
#
[docs]def select_first_expression():
""" There is always at least one (???), so no checking of bounds.
"""
sel.click(sel.elements("//a[contains(@id,'exp_')]", root=_expressions_root())[0])
[docs]def select_expression_by_text(text):
sel.click(
sel.element(
"//a[contains(@id,'exp_')][contains(normalize-space(text()),'{}')]".format(text),
root=_expressions_root()
)
)
[docs]def no_expression_present():
els = sel.elements("//a[contains(@id,'exp_')]", root=_expressions_root())
if len(els) > 1:
return False
return els[0].text.strip() == "???"
[docs]def any_expression_present():
return not no_expression_present()
[docs]def is_editing():
try:
sel.element(
"//a[contains(@id,'exp_')][contains(normalize-space(text()),'???')]",
root=_expressions_root()
)
return True
except NoSuchElementException:
return False
[docs]def delete_whole_expression():
while any_expression_present():
select_first_expression()
click_remove()
[docs]def get_expression_as_text():
""" Returns whole expression as represented visually.
"""
return sel.text("//div[@id='exp_editor_div']/fieldset/div").encode("utf-8").strip()
###
# Form handling
#
field_form = Form(
fields=[
("type", AngularSelect("chosen_typ")),
("field", AngularSelect("chosen_field")),
("key", AngularSelect("chosen_key")),
("value", Input("chosen_value")),
("user_input", Input("user_input")),
]
)
field_date_form = Form(
fields=[
("dropdown_select", AngularSelect("chosen_from_1")),
("input_select_date", Calendar("miq_date_1_0")),
("input_select_time", AngularSelect("miq_time_1_0"))
]
)
count_form = Form(
fields=[
("type", AngularSelect("chosen_typ")),
("count", AngularSelect("chosen_count")),
("key", AngularSelect("chosen_key", exact=True)),
("value", Input("chosen_value")),
("user_input", Input("user_input")),
]
)
tag_form = Form(
fields=[
("type", AngularSelect("chosen_typ")),
("tag", AngularSelect("chosen_tag")),
("value", AngularSelect("chosen_value")),
("user_input", Input("user_input")),
]
)
find_form = Form(
fields=[
("type", AngularSelect("chosen_typ")),
("field", AngularSelect("chosen_field")),
("skey", AngularSelect("chosen_skey")),
("value", "#chosen_value"),
("check", AngularSelect("chosen_check")),
("cfield", AngularSelect("chosen_cfield", exact=True)),
("ckey", AngularSelect("chosen_ckey")),
("cvalue", Input("chosen_cvalue")),
]
)
registry_form = Form(
fields=[
("type", AngularSelect("chosen_typ")),
("key", Input("chosen_regkey")),
("value", Input("chosen_regval")),
("operation", AngularSelect("chosen_key")),
("contents", Input("chosen_value")),
]
)
date_switch_buttons = Region(
locators=dict(
to_relative="//img[@alt='Click to change to a relative Date/Time format']",
to_specific="//img[@alt='Click to change to a specific Date/Time format']"
)
)
date_specific_form = Form(
fields=[
("date", Calendar("miq_date_1_0")),
("time", AngularSelect("miq_time_1_0")),
]
)
date_relative_form = Form(
fields=[
("from", AngularSelect("chosen_from_1")),
("through", AngularSelect("chosen_through_1")),
]
)
###
# Fill commands
#
[docs]def fill_count(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`.
"""
fill(
count_form,
dict(
type="Count of",
count=count,
key=key,
value=int(value) if value is not None else value,
),
)
# In case of advanced search box
if sel.is_displayed(field_form.user_input):
user_input = value is None
else:
user_input = None
fill(field_form.user_input, user_input)
sel.click(buttons.commit)
[docs]def fill_tag(tag=None, value=None):
""" Fills the 'Tag' type of form.
Args:
tag: Name of the field to compare.
value: Value to check against.
Returns: See :py:func:`cfme.web_ui.fill`.
"""
fill(
tag_form,
dict(
type="Tag",
tag=tag,
value=value,
),
)
# In case of advanced search box
if sel.is_displayed(field_form.user_input):
user_input = value is None
else:
user_input = None
fill(field_form.user_input, user_input)
sel.click(buttons.commit)
[docs]def fill_registry(key=None, value=None, operation=None, contents=None):
""" Fills the 'Registry' type of form."""
return fill(
registry_form,
dict(
type="Registry",
key=key,
value=value,
operation=operation,
contents=contents,
),
action=buttons.commit
)
[docs]def fill_find(field=None, skey=None, value=None, check=None, cfield=None, ckey=None, cvalue=None):
fill(
find_form,
dict(
type="Find",
field=field,
skey=skey,
value=value,
check=check,
cfield=cfield,
ckey=ckey,
cvalue=cvalue,))
sel.click(buttons.commit)
[docs]def fill_field(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:
no_date = False
else:
no_date = True
fill(
field_form,
dict(
type="Field",
field=field,
key=key,
value=value if no_date else None,
),
)
# In case of advanced search box
if sel.is_displayed(field_form.user_input):
user_input = value is None
else:
user_input = None
fill(field_form.user_input, user_input)
if not no_date:
# Flip the right part of form
if isinstance(value, basestring) and not re.match(r"^[0-9]{2}/[0-9]{2}/[0-9]{4}$", value):
if not sel.is_displayed(field_date_form.dropdown_select):
sel.click(date_switch_buttons.to_relative)
fill(field_date_form, {"dropdown_select": value})
sel.click(buttons.commit)
else:
# Specific selection
if not sel.is_displayed(field_date_form.input_select_date):
sel.click(date_switch_buttons.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
fill(field_date_form.input_select_date, date)
# Try waiting a little bit for time field
# If we don't wait, committing the expression will glitch
try:
wait_for(lambda: sel.is_displayed(field_date_form.input_select_time), num_sec=6)
# It appeared, so if the time is to be set, we will set it (passing None glitches)
if time:
fill(field_date_form.input_select_time, time)
except TimedOutError:
# Did not appear, ignore that
pass
finally:
# And finally, commit the expression :)
sel.click(buttons.commit)
else:
sel.click(buttons.commit)
###
# Processor for YAML commands
#
_banned_commands = {"get_func", "run_commands", "dsl_parse", "create_program_from_dsl"}
[docs]def get_func(name):
""" Return callable from this module by its name.
Args:
name: Name of the variable containing the callable.
Returns: Callable from this module
"""
assert name not in _banned_commands, "Command '{}' is not permitted!".format(name)
assert not name.startswith("_"), "Command '{}' is private!".format(name)
try:
func = getattr(sys.modules[__name__], 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):
""" 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`)
"""
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))
elif isinstance(command, dict):
for key, value in command.iteritems():
func = get_func(key)
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:
delete_whole_expression()
for step in step_list:
step()
@singledispatch
def create_program(source):
""" Wrong call
"""
raise TypeError("Program code wrong! You must specify string (DSL), command list or None!")
@create_program.method(basestring)
def _create_program_from_dsl(dsl_program):
""" 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]*$"
ARGS_CALL = r"^(?P<name>[a-z_A-Z][a-z_A-Z0-9]*)\((?P<args>.*)\)$"
KWARG = r"^[^=]+=.*$"
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(command_list)
@create_program.method(list)
@create_program.method(tuple)
def _create_program_from_list(command_list):
""" 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)
@create_program.method(types.NoneType)
def _create_program_from_none(none):
return lambda: none
[docs]class Expression(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.
"""
pretty_attrs = ['show_func']
def __init__(self, show_func=lambda: None):
self.show_func = show_func
@fill.method((Expression, Anything))
def _fill_expression(e, p):
e.show_func()
prog = create_program(p)
prog()