safe/gui/tools/print_report_dialog.py
# coding=utf-8
"""This is a dialog to print a custom map report.
"""
import logging
from copy import deepcopy
from os import listdir
from os.path import join, exists, splitext, dirname
from qgis.PyQt import QtGui, QtWidgets, QtCore, QtXml
from qgis.PyQt.QtWidgets import QFileDialog
from qgis.core import (
QgsApplication,
QgsPrintLayout,
QgsProject,
QgsReadWriteContext)
from safe import messaging as m
from safe.common.signals import send_error_message, send_static_message
from safe.definitions.constants import ANALYSIS_FAILED_BAD_CODE
from safe.definitions.exposure import exposure_population
from safe.definitions.reports import (
pdf_product_tag, final_product_tag, html_product_tag, qpt_product_tag)
from safe.definitions.reports.components import (
map_report,
infographic_report,
standard_impact_report_metadata_pdf,
all_default_report_components,
standard_multi_exposure_impact_report_metadata_pdf,
impact_report_pdf_component,
action_checklist_pdf_component,
analysis_provenance_details_pdf_component)
from safe.definitions.utilities import (
override_component_template, definition, update_template_component)
from safe.gui.tools.help.impact_report_help import impact_report_help
from safe.impact_function.impact_function_utilities import report_urls
from safe.impact_function.multi_exposure_wrapper import (
MultiExposureImpactFunction)
from safe.messaging import styles
from safe.report.impact_report import ImpactReport
from safe.report.report_metadata import (
QgisComposerComponentsMetadata)
from safe.utilities.i18n import tr
from safe.utilities.resources import (
get_ui_class, resources_path, html_header, html_footer)
from safe.utilities.settings import setting
__copyright__ = "Copyright 2017, The InaSAFE Project"
__license__ = "GPL version 3"
__email__ = "info@inasafe.org"
__revision__ = '$Format:%H$'
INFO_STYLE = styles.BLUE_LEVEL_4_STYLE
FORM_CLASS = get_ui_class('print_report_dialog.ui')
LOGGER = logging.getLogger('InaSAFE')
class PrintReportDialog(QtWidgets.QDialog, FORM_CLASS):
"""Print report dialog for the InaSAFE plugin."""
def __init__(self, impact_function, iface, dock=None, parent=None):
"""Constructor for the dialog.
:param iface: A Quantum GIS QgisAppInterface instance.
:type iface: QgisAppInterface
:param parent: Parent widget of this dialog
:type parent: QWidget
:param dock: Optional dock widget instance that we can notify of
changes to the keywords.
:type dock: Dock
.. versionadded: 4.3.0
"""
QtWidgets.QDialog.__init__(self, parent)
self.setupUi(self)
# Save reference to the QGIS interface and parent
self.iface = iface
self.parent = parent
self.dock = dock
self.impact_function = impact_function
self.create_pdf = False
self.all_checkboxes = {
impact_report_pdf_component['key']:
self.impact_summary_checkbox,
action_checklist_pdf_component['key']:
self.action_checklist_checkbox,
analysis_provenance_details_pdf_component['key']:
self.provenance_checkbox,
infographic_report['key']:
self.infographic_checkbox
}
# setup checkboxes, all checkboxes are checked by default
for checkbox in list(self.all_checkboxes.values()):
checkbox.setChecked(True)
# override template is selected by default
self.default_template_radio.setChecked(True)
self.is_population = False
self.is_multi_exposure = isinstance(
self.impact_function, MultiExposureImpactFunction)
override_template_found = None
population_found = False
if self.is_multi_exposure:
self.override_template_radio.setEnabled(False)
self.override_template_label.setEnabled(False)
self.override_template_found_label.setEnabled(False)
# below features are currently not applicable for multi-exposure IF
self.action_checklist_checkbox.setEnabled(False)
self.action_checklist_checkbox.setChecked(False)
self.provenance_checkbox.setEnabled(False)
self.provenance_checkbox.setChecked(False)
provenances = [
analysis.provenance for analysis in (
self.impact_function.impact_functions)]
for provenance in provenances:
exposure_keywords = provenance['exposure_keywords']
exposure_type = definition(exposure_keywords['exposure'])
if exposure_type == exposure_population:
population_found = True
break
self.infographic_checkbox.setEnabled(population_found)
self.infographic_checkbox.setChecked(population_found)
else:
# search for available override template
hazard_type = definition(
self.impact_function.provenance['hazard_keywords'][
'hazard'])
exposure_type = definition(
self.impact_function.provenance['exposure_keywords'][
'exposure'])
# noinspection PyArgumentList
custom_template_dir = join(
QgsApplication.qgisSettingsDirPath(), 'inasafe')
if exists(custom_template_dir) and hazard_type and exposure_type:
for filename in listdir(custom_template_dir):
file_name, file_format = splitext(filename)
if file_format[1:] != (
QgisComposerComponentsMetadata.OutputFormat.QPT):
continue
if hazard_type['key'] in file_name and (
exposure_type['key'] in file_name):
override_template_found = filename
# check for population exposure
self.is_population = exposure_type == exposure_population
self.infographic_checkbox.setEnabled(
self.is_population or population_found)
if override_template_found:
string_format = tr('*Template override found: {template_path}')
self.override_template_found_label.setText(
string_format.format(template_path=override_template_found))
else:
self.override_template_radio.setEnabled(False)
# additional buttons
self.button_print_pdf = QtWidgets.QPushButton(tr('Open as PDF'))
self.button_print_pdf.setObjectName('button_print_pdf')
self.button_print_pdf.setToolTip(tr(
'Write report to PDF and open it in default viewer'))
self.button_box.addButton(
self.button_print_pdf, QtWidgets.QDialogButtonBox.ActionRole)
self.template_chooser.clicked.connect(self.template_chooser_clicked)
self.button_print_pdf.clicked.connect(self.accept)
self.button_open_composer.clicked.connect(self.accept)
# self.no_map_radio.toggled.connect(self.toggle_template_selector)
# self.no_map_radio.toggled.connect(
# self.button_open_composer.setDisabled)
self.default_template_radio.toggled.connect(
self.toggle_template_selector)
self.override_template_radio.toggled.connect(
self.toggle_template_selector)
self.search_directory_radio.toggled.connect(
self.toggle_template_selector)
self.search_on_disk_radio.toggled.connect(
self.toggle_template_selector)
# Set up things for context help
self.help_button = self.button_box.button(
QtWidgets.QDialogButtonBox.Help)
# Allow toggling the help button
self.help_button.setCheckable(True)
self.help_button.toggled.connect(self.help_toggled)
self.main_stacked_widget.setCurrentIndex(1)
self.unwanted_templates = ['merged_report.qpt', 'infographic.qpt']
# Load templates from resources...
template_dir_path = resources_path('qgis-composer-templates')
self.populate_template_combobox(
template_dir_path, self.unwanted_templates)
# ...and user directory
default_path = join(QgsApplication.qgisSettingsDirPath(), 'inasafe')
path = setting(
'inasafe/reportTemplatePath', default_path, expected_type=str)
if exists(path):
self.populate_template_combobox(path)
self.restore_state()
def populate_template_combobox(self, path, unwanted_templates=None):
"""Helper method for populating template combobox.
:param unwanted_templates: List of templates that isn't an option.
:type unwanted_templates: list
.. versionadded: 4.3.0
"""
templates_dir = QtCore.QDir(path)
templates_dir.setFilter(
QtCore.QDir.Files
| QtCore.QDir.NoSymLinks
| QtCore.QDir.NoDotAndDotDot)
templates_dir.setNameFilters(['*.qpt', '*.QPT'])
report_files = templates_dir.entryList()
if not unwanted_templates:
unwanted_templates = []
for unwanted_template in unwanted_templates:
if unwanted_template in report_files:
report_files.remove(unwanted_template)
for f in report_files:
self.template_combo.addItem(
QtCore.QFileInfo(f).baseName(), path + '/' + f)
def restore_state(self):
"""Reinstate the options based on the user's stored session info.
.. versionadded: 4.3.0
"""
settings = QtCore.QSettings()
flag = bool(settings.value(
'inasafe/useDefaultTemplates', True, type=bool))
self.default_template_radio.setChecked(flag)
try:
default_template_path = resources_path(
'qgis-composer-templates', 'inasafe-map-report-portrait.qpt')
path = settings.value(
'inasafe/lastTemplate',
default_template_path,
type=str)
self.template_combo.setCurrentIndex(
self.template_combo.findData(path))
except TypeError:
self.template_combo.setCurrentIndex(2)
try:
path = settings.value('inasafe/lastCustomTemplate', '', type=str)
except TypeError:
path = ''
self.template_path.setText(path)
def save_state(self):
"""Store the options into the user's stored session info.
.. versionadded: 4.3.0
"""
settings = QtCore.QSettings()
settings.setValue(
'inasafe/useDefaultTemplates',
self.default_template_radio.isChecked())
settings.setValue(
'inasafe/lastTemplate',
self.template_combo.itemData(self.template_combo.currentIndex()))
settings.setValue(
'inasafe/lastCustomTemplate', self.template_path.text())
def retrieve_paths(self, products, report_path, suffix=None):
"""Helper method to retrieve path from particular report metadata.
:param products: Report products.
:type products: list
:param report_path: Path of the IF output.
:type report_path: str
:param suffix: Expected output product file type (extension).
:type suffix: str
:return: List of absolute path of the output product.
:rtype: list
"""
paths = []
for product in products:
path = ImpactReport.absolute_output_path(
join(report_path, 'output'),
products,
product.key)
if isinstance(path, list):
for p in path:
paths.append(p)
elif isinstance(path, dict):
for p in list(path.values()):
paths.append(p)
else:
paths.append(path)
if suffix:
paths = [p for p in paths if p.endswith(suffix)]
paths = [p for p in paths if exists(p)]
return paths
def open_as_pdf(self):
"""Print the selected report as a PDF product.
.. versionadded: 4.3.0
"""
# Get output path from datastore
report_urls_dict = report_urls(self.impact_function)
# get report urls for each product tag as list
for key, value in list(report_urls_dict.items()):
report_urls_dict[key] = list(value.values())
if self.dock:
# create message to user
status = m.Message(
m.Heading(self.dock.tr('Map Creator'), **INFO_STYLE),
m.Paragraph(self.dock.tr(
'Your PDF was created....opening using the default PDF '
'viewer on your system.')),
m.ImportantText(self.dock.tr(
'The generated pdfs were saved '
'as:')))
for path in report_urls_dict.get(pdf_product_tag['key'], []):
status.add(m.Paragraph(path))
status.add(m.Paragraph(
m.ImportantText(
self.dock.tr('The generated htmls were saved as:'))))
for path in report_urls_dict.get(html_product_tag['key'], []):
status.add(m.Paragraph(path))
status.add(m.Paragraph(
m.ImportantText(
self.dock.tr('The generated qpts were saved as:'))))
for path in report_urls_dict.get(qpt_product_tag['key'], []):
status.add(m.Paragraph(path))
send_static_message(self.dock, status)
for path in report_urls_dict.get(pdf_product_tag['key'], []):
# noinspection PyCallByClass,PyTypeChecker,PyTypeChecker
QtGui.QDesktopServices.openUrl(
QtCore.QUrl.fromLocalFile(path))
def open_in_composer(self):
"""Open in layout designer a given MapReport instance.
.. versionadded: 4.3.0
"""
impact_layer = self.impact_function.analysis_impacted
report_path = dirname(impact_layer.source())
impact_report = self.impact_function.impact_report
custom_map_report_metadata = impact_report.metadata
custom_map_report_product = (
custom_map_report_metadata.component_by_tags(
[final_product_tag, pdf_product_tag]))
for template_path in self.retrieve_paths(
custom_map_report_product,
report_path=report_path,
suffix='.qpt'):
layout = QgsPrintLayout(QgsProject.instance())
with open(template_path) as template_file:
template_content = template_file.read()
document = QtXml.QDomDocument()
document.setContent(template_content)
# load layout object
rwcontext = QgsReadWriteContext()
load_status = layout.loadFromTemplate(document, rwcontext)
if not load_status:
# noinspection PyCallByClass,PyTypeChecker
QtWidgets.QMessageBox.warning(
self,
tr('InaSAFE'),
tr('Error loading template: %s') % template_path)
return
QgsProject.instance().layoutManager().addLayout(layout)
self.iface.openLayoutDesigner(layout)
def prepare_components(self):
"""Prepare components that are going to be generated based on
user options.
:return: Updated list of components.
:rtype: dict
"""
# Register the components based on user option
# First, tabular report
generated_components = deepcopy(all_default_report_components)
# Rohmat: I need to define the definitions here, I can't get
# the definition using definition helper method.
component_definitions = {
impact_report_pdf_component['key']:
impact_report_pdf_component,
action_checklist_pdf_component['key']:
action_checklist_pdf_component,
analysis_provenance_details_pdf_component['key']:
analysis_provenance_details_pdf_component,
infographic_report['key']: infographic_report
}
duplicated_report_metadata = None
for key, checkbox in list(self.all_checkboxes.items()):
if not checkbox.isChecked():
component = component_definitions[key]
if component in generated_components:
generated_components.remove(component)
continue
if self.is_multi_exposure:
impact_report_metadata = (
standard_multi_exposure_impact_report_metadata_pdf)
else:
impact_report_metadata = (
standard_impact_report_metadata_pdf)
if component in impact_report_metadata['components']:
if not duplicated_report_metadata:
duplicated_report_metadata = deepcopy(
impact_report_metadata)
duplicated_report_metadata['components'].remove(
component)
if impact_report_metadata in generated_components:
generated_components.remove(
impact_report_metadata)
generated_components.append(
duplicated_report_metadata)
# Second, custom and map report
# Get selected template path to use
selected_template_path = None
if self.search_directory_radio.isChecked():
selected_template_path = self.template_combo.itemData(
self.template_combo.currentIndex())
elif self.search_on_disk_radio.isChecked():
selected_template_path = self.template_path.text()
if not exists(selected_template_path):
# noinspection PyCallByClass,PyTypeChecker
QtWidgets.QMessageBox.warning(
self,
tr('InaSAFE'),
tr(
'Please select a valid template before printing. '
'The template you choose does not exist.'))
if map_report in generated_components:
# if self.no_map_radio.isChecked():
# generated_components.remove(map_report)
if self.default_template_radio.isChecked():
# make sure map report is there
generated_components.append(
generated_components.pop(
generated_components.index(map_report)))
elif self.override_template_radio.isChecked():
hazard_type = definition(
self.impact_function.provenance['hazard_keywords'][
'hazard'])
exposure_type = definition(
self.impact_function.provenance['exposure_keywords'][
'exposure'])
generated_components.remove(map_report)
generated_components.append(
update_template_component(
component=map_report,
hazard=hazard_type,
exposure=exposure_type))
elif selected_template_path:
generated_components.remove(map_report)
generated_components.append(
override_component_template(
map_report, selected_template_path))
return generated_components
def accept(self):
"""Method invoked when OK button is clicked."""
self.save_state()
self.dock.show_busy()
# The order of the components are matter.
components = self.prepare_components()
error_code, message = self.impact_function.generate_report(
components, iface=self.iface)
if error_code == ImpactReport.REPORT_GENERATION_FAILED:
self.dock.hide_busy()
LOGGER.info(tr(
'The impact report could not be generated.'))
send_error_message(self, message)
LOGGER.info(message.to_text())
return ANALYSIS_FAILED_BAD_CODE, message
sender_name = self.sender().objectName()
try:
if sender_name == 'button_print_pdf':
self.create_pdf = True
self.open_as_pdf()
else:
self.create_pdf = False
self.open_in_composer()
self.dock.hide_busy()
except Exception:
self.dock.hide_busy()
QtWidgets.QDialog.accept(self)
def template_chooser_clicked(self):
"""Slot activated when report file tool button is clicked.
.. versionadded: 4.3.0
"""
path = self.template_path.text()
if not path:
path = setting('lastCustomTemplate', '', str)
if path:
directory = dirname(path)
else:
directory = ''
# noinspection PyCallByClass,PyTypeChecker
file_name = QFileDialog.getOpenFileName(
self,
tr('Select report'),
directory,
tr('QGIS composer templates (*.qpt *.QPT)'))
self.template_path.setText(file_name)
def toggle_template_selector(self):
"""Slot for template selector elements behaviour.
.. versionadded: 4.3.0
"""
if self.search_directory_radio.isChecked():
self.template_combo.setEnabled(True)
else:
self.template_combo.setEnabled(False)
if self.search_on_disk_radio.isChecked():
self.template_path.setEnabled(True)
self.template_chooser.setEnabled(True)
else:
self.template_path.setEnabled(False)
self.template_chooser.setEnabled(False)
def help_toggled(self, flag):
"""Show or hide the help tab in the stacked widget.
:param flag: Flag indicating whether help should be shown or hidden.
:type flag: bool
.. versionadded: 4.3.0
"""
if flag:
self.help_button.setText(tr('Hide Help'))
self.show_help()
else:
self.help_button.setText(tr('Show Help'))
self.hide_help()
def hide_help(self):
"""Hide the usage info from the user.
.. versionadded: 4.3.0
"""
self.main_stacked_widget.setCurrentIndex(1)
def show_help(self):
"""Show usage info to the user.
.. versionadded: 4.3.0
"""
# Read the header and footer html snippets
self.main_stacked_widget.setCurrentIndex(0)
header = html_header()
footer = html_footer()
string = header
message = impact_report_help()
string += message.to_html()
string += footer
self.help_web_view.setHtml(string)