# -*- coding: utf-8 -*-
"""Page model for Cloud Intel / Reports / Reports"""
import attr
from cached_property import cached_property
from navmazing import NavigateToAttribute, NavigateToSibling
from widgetastic.exceptions import NoSuchElementException
from widgetastic.utils import ParametrizedLocator, VersionPick, Version
from widgetastic.widget import Text, Checkbox, View, ParametrizedView, Table as VanillaTable
from widgetastic_patternfly import Button, Input, BootstrapSelect, Tab, CandidateNotFound
from cfme.modeling.base import BaseCollection, BaseEntity
from cfme.utils import ParamClassName
from cfme.utils.appliance.implementations.ui import navigator, CFMENavigateStep, navigate_to
from cfme.utils.blockers import BZ
from cfme.utils.pretty import Pretty
from cfme.utils.timeutil import parsetime
from cfme.utils.update import Updateable
from cfme.utils.wait import wait_for
from widgetastic_manageiq import (PaginationPane, Table, ReportToolBarViewSelector,
NonJSPaginationPane)
from widgetastic_manageiq.expression_editor import ExpressionEditor
from . import CloudIntelReportsView, ReportsMultiBoxSelect
[docs]class ReportAddView(CustomReportFormCommon):
add_button = Button("Add")
@property
def is_displayed(self):
return (
self.in_intel_reports and
self.reports.is_opened and
self.report_title.text == "Adding a new Report" and
self.reports.tree.currently_selected == ["All Reports"]
)
[docs]class ReportEditView(CustomReportFormCommon):
save_button = Button("Save")
@property
def is_displayed(self):
return (
self.in_intel_reports and
self.reports.is_opened and
self.reports.tree.currently_selected == [
"All Reports",
self.context["object"].company_name,
"Custom",
self.context["object"].menu_name
] and
self.report_title.text == 'Editing Report "{}"'.format(self.context["object"].menu_name)
)
[docs]class ReportDetailsView(CloudIntelReportsView):
title = Text("#explorer_title_text")
reload_button = Button(
title=VersionPick({
Version.lowest(): 'Reload current display',
'5.9': 'Refresh this page'
})
)
paginator = PaginationPane()
@View.nested
class report_info(Tab): # noqa
TAB_NAME = "Report Info"
queue_button = Button("Queue")
@View.nested
class saved_reports(Tab): # noqa
TAB_NAME = "Saved Reports"
table = Table(".//div[@id='records_div' or @class='miq-data-table']/table")
paginator = PaginationPane()
@property
def is_displayed(self):
return (
self.in_intel_reports and
self.reports.is_opened and
self.report_info.is_active() and
self.reports.tree.currently_selected == [
"All Reports",
self.context["object"].type or self.context["object"].company_name,
self.context["object"].subtype or "Custom",
self.context["object"].menu_name
] and
self.title.text == 'Report "{}"'.format(self.context["object"].menu_name)
)
[docs]class SavedReportDetailsView(CloudIntelReportsView):
title = Text("#explorer_title_text")
table = VanillaTable(".//div[@id='report_html_div']/table")
# PaginationPane() is not working on Report Details page
paginator = View.nested(NonJSPaginationPane)
view_selector = View.nested(ReportToolBarViewSelector)
@ParametrizedView.nested
class download(ParametrizedView): # noqa
PARAMETERS = ("format", )
ALL_LINKS = ".//a[starts-with(@name, 'download_choice__render_report_')]"
download_button = Button(title="Download")
link = Text(ParametrizedLocator(".//a[normalize-space()={format|quote}]"))
def __init__(self, *args, **kwargs):
ParametrizedView.__init__(self, *args, **kwargs)
self.download_button.click()
self.link.click()
@classmethod
def all(cls, browser):
return [(browser.text(e), ) for e in browser.elements(cls.ALL_LINKS)]
@property
def is_displayed(self):
return (
self.in_intel_reports and
self.reports.is_opened and
self.reports.tree.currently_selected == [
"All Reports",
self.context["object"].report.type or self.context["object"].report.company_name,
self.context["object"].report.subtype or "Custom",
self.context["object"].report.menu_name,
self.context["object"].datetime_in_tree
] and
self.title.text == 'Saved Report "{} - {}"'.format(
self.context["object"].report.title,
self.context["object"].queued_datetime_in_title
)
)
[docs]class AllReportsView(CloudIntelReportsView):
title = Text("#explorer_title_text")
reports_table = VanillaTable(".//div[@id='report_list_div']/table")
@property
def is_displayed(self):
return (
self.in_intel_reports and
self.reports.is_opened and
self.reports.tree.currently_selected == ["All Reports"] and
self.title.text == "All Reports" and
self.reports_table.is_displayed
)
[docs]class AllCustomReportsView(CloudIntelReportsView):
title = Text("#explorer_title_text")
@property
def is_displayed(self):
return (
self.in_intel_reports and
self.reports.is_opened and
self.reports.tree.currently_selected == [
"All Reports",
self.context["object"].company_name,
"Custom"
] and
self.title.text == "Custom Reports"
)
@attr.s
[docs]class Report(BaseEntity, Updateable):
_param_name = ParamClassName('title')
menu_name = attr.ib(default=None)
title = attr.ib(default=None)
company_name = attr.ib()
type = attr.ib(default=None)
subtype = attr.ib(default=None)
base_report_on = attr.ib(default=None)
report_fields = attr.ib(default=None)
cancel_after = attr.ib(default=None)
consolidation = attr.ib(default=None)
formatting = attr.ib(default=None)
styling = attr.ib(default=None)
filter = attr.ib(default=None)
filter_show_costs = attr.ib(default=None)
filter_owner = attr.ib(default=None)
filter_tag_cat = attr.ib(default=None)
filter_tag_value = attr.ib(default=None)
interval = attr.ib(default=None)
interval_size = attr.ib(default=None)
interval_end = attr.ib(default=None)
sort = attr.ib(default=None)
chart_type = attr.ib(default=None)
top_values = attr.ib(default=None)
sum_other = attr.ib(default=None)
base_timeline_on = attr.ib(default=None)
band_units = attr.ib(default=None)
event_position = attr.ib(default=None)
show_event_unit = attr.ib(default=None)
show_event_count = attr.ib(default=None)
summary = attr.ib(default=None)
charts = attr.ib(default=None)
timeline = attr.ib(default=None)
is_candu = attr.ib(default=False)
def __attrs_post_init__(self):
self._collections = {'saved_reports': SavedReportsCollection}
@company_name.default
[docs] def company_name_default(self):
if self.appliance.version < "5.9":
return "My Company (All EVM Groups)"
else:
return "My Company (All Groups)"
[docs] def update(self, updates):
view = navigate_to(self, "Edit")
changed = view.fill(updates)
if changed:
view.save_button.click()
else:
view.cancel_button.click()
view = self.create_view(ReportDetailsView, override=updates)
assert view.is_displayed
view.flash.assert_no_error()
if changed:
view.flash.assert_message(
'Report "{}" was saved'.format(self.menu_name))
else:
view.flash.assert_message(
'Edit of Report "{}" was cancelled by the user'.format(self.menu_name))
[docs] def delete(self, cancel=False):
view = navigate_to(self, "Details")
node = view.reports.tree.expand_path("All Reports", self.company_name, "Custom")
custom_reports_number = len(view.reports.tree.child_items(node))
view.configuration.item_select("Delete this Report from the Database",
handle_alert=not cancel)
if cancel:
assert view.is_displayed
view.flash.assert_no_error()
else:
# This check is needed because after deleting the last custom report,
# the whole "My Company (All EVM Groups)" branch in the tree will be removed.
if custom_reports_number > 1:
view = self.create_view(AllCustomReportsView)
assert view.is_displayed
view.flash.assert_no_error()
if not BZ(1561779, forced_streams=['5.9', '5.8']).blocks:
view.flash.assert_message(
'Report "{}": Delete successful'.format(self.menu_name))
@cached_property
def saved_reports(self):
return self.collections.saved_reports
[docs] def queue(self, wait_for_finish=False):
view = navigate_to(self, "Details")
view.report_info.queue_button.click()
view.flash.assert_no_error()
if wait_for_finish:
# Get the queued_at value to always target the correct row
queued_at = view.saved_reports.table[0]["Queued At"].text
def _get_state():
row = view.saved_reports.table.row(queued_at=queued_at)
status = row.status.text.strip().lower()
assert status != "error"
return status == "complete"
wait_for(
_get_state,
delay=1,
message="wait for report generation finished",
fail_func=view.reload_button.click,
num_sec=300,
)
view.reload_button.click()
first_row = view.saved_reports.table[0]
saved_report = self.saved_reports.instantiate(
first_row.run_at.text,
first_row.queued_at.text,
self.is_candu
)
return saved_report
@property
def exists(self):
try:
navigate_to(self, "Details")
return True
except CandidateNotFound:
return False
@attr.s
[docs]class ReportsCollection(BaseCollection):
ENTITY = Report
[docs] def create(self, **values):
view = navigate_to(self, "Add")
view.fill(values)
view.add_button.click()
view = self.create_view(AllReportsView)
assert view.is_displayed
view.flash.assert_no_error()
view.flash.assert_message('Report "{}" was added'.format(values["menu_name"]))
return self.instantiate(**values)
@attr.s
[docs]class SavedReport(Updateable, BaseEntity):
"""Custom Saved Report. Enables us to retrieve data from the table.
Args:
run_datetime: Datetime of "Run At" of the report. That's what :py:func:`queue` returns.
queued_datetime: Datetime of "Queued At" of the report.
candu: If it is a C&U report, in that case it uses a different table.
"""
run_datetime = attr.ib()
queued_datetime = attr.ib()
candu = attr.ib(default=False)
@property
def parent_obj(self):
return self.parent.parent
@property
def report(self):
return self.parent_obj
@cached_property
def queued_datetime_in_title(self):
return parsetime.from_american_with_utc(self.queued_datetime).to_saved_report_title_format()
@cached_property
def datetime_in_tree(self):
return parsetime.from_american_with_utc(self.run_datetime).to_iso_with_utc()
@cached_property
def data(self):
"""Retrieves data from the saved report.
Returns: :py:class:`SavedReportData`.
"""
view = navigate_to(self, "Details")
if 'No records found for this report' in view.flash.read():
# No data found
return SavedReportData([], [])
view.paginator.set_items_per_page(1000)
try:
headers = tuple([hdr.encode("utf-8") for hdr in view.table.headers])
body = []
for _ in view.paginator.pages():
for row in view.table.rows():
if not all([c[1].is_displayed for c in row]):
# This is a temporary workaround for cases we have row span
# greater that 1 column (e.g. in case of "Totals: ddd" column).
# TODO: Support this functionality in widgetastic. Issue:
# https://github.com/RedHatQE/widgetastic.core/issues/26
continue
row_data = tuple([row[header].text.encode("utf-8") for header in headers])
body.append(row_data)
except NoSuchElementException:
# No data found
return SavedReportData([], [])
else:
return SavedReportData(headers, body)
[docs] def download(self, extension):
extensions_mapping = {"txt": "Text", "csv": "CSV", "pdf": "PDF"}
view = navigate_to(self, "Details")
view.download("Download as {}".format(extensions_mapping[extension]))
[docs] def delete(self, cancel=False):
view = navigate_to(self, "Details")
view.configuration.item_select(
"Delete this Saved Report from the Database",
handle_alert=not cancel
)
if cancel:
assert view.is_displayed
view.flash.assert_no_error()
else:
view.flash.assert_no_error()
# TODO Doesn't work due to this BZ https://bugzilla.redhat.com/show_bug.cgi?id=1489387
# view.flash.assert_message("Successfully deleted Saved Report from the CFME Database")
@property
def exists(self):
try:
return bool(navigate_to(self, "Details"))
except CandidateNotFound:
return False
@attr.s
[docs]class SavedReportsCollection(BaseCollection):
ENTITY = SavedReport
[docs] def all(self):
view = navigate_to(self.parent, "Details")
results = []
try:
for _ in view.saved_reports.paginator.pages():
for row in view.saved_reports.table.rows():
if not all([c[1].is_displayed for c in row]):
# This is a temporary workaround for cases we have row span
# greater that 1 column (e.g. in case of "Totals: ddd" column).
# TODO: Support this functionality in widgetastic. Issue:
# https://github.com/RedHatQE/widgetastic.core/issues/26
continue
results.append(
self.instantiate(
row.run_at.text.encode("utf-8"),
row.queued_at.text.encode("utf-8"),
self.parent.is_candu
)
)
except NoSuchElementException:
pass
return results
[docs]class SavedReportData(Pretty):
"""This class stores data retrieved from saved report.
Args:
headers: Tuple with header columns.
body: List of tuples with body rows.
"""
pretty_attrs = ["headers", "body"]
def __init__(self, headers, body):
self.headers = headers
self.body = body
@property
def rows(self):
for row in self.body:
yield dict(zip(self.headers, row))
[docs] def find_row(self, column, value):
if column not in self.headers:
return None
for row in self.rows:
if row[column] == value:
return row
[docs] def find_cell(self, column, value, cell):
try:
return self.find_row(column, value)[cell]
except TypeError:
return None
@navigator.register(ReportsCollection, "All")
[docs]class ReportsAll(CFMENavigateStep):
VIEW = AllReportsView
prerequisite = NavigateToAttribute("appliance.server", "CloudIntelReports")
[docs] def step(self):
self.prerequisite_view.reports.tree.click_path("All Reports")
@navigator.register(ReportsCollection, "Add")
[docs]class ReportsNew(CFMENavigateStep):
VIEW = ReportAddView
prerequisite = NavigateToAttribute("appliance.server", "CloudIntelReports")
[docs] def step(self):
self.prerequisite_view.reports.tree.click_path("All Reports")
self.prerequisite_view.configuration.item_select("Add a new Report")
@navigator.register(Report, "Edit")
[docs]class ReportEdit(CFMENavigateStep):
VIEW = ReportEditView
prerequisite = NavigateToSibling("Details")
[docs] def step(self):
self.prerequisite_view.configuration.item_select("Edit this Report")
@navigator.register(Report, "Details")
[docs]class ReportDetails(CFMENavigateStep):
VIEW = ReportDetailsView
prerequisite = NavigateToAttribute("appliance.server", "CloudIntelReports")
[docs] def step(self):
self.prerequisite_view.reports.tree.click_path(
"All Reports",
self.obj.type or self.obj.company_name,
self.obj.subtype or "Custom",
self.obj.menu_name
)
self.view.report_info.select()
@navigator.register(SavedReport, "Details")
[docs]class SavedReportDetails(CFMENavigateStep):
VIEW = SavedReportDetailsView
prerequisite = NavigateToAttribute("appliance.server", "CloudIntelReports")
[docs] def step(self):
self.prerequisite_view.reports.tree.click_path(
"All Reports",
self.obj.report.type or self.obj.report.company_name,
self.obj.report.subtype or "Custom",
self.obj.report.menu_name,
self.obj.datetime_in_tree
)