Source code for spinetoolbox.project_items.exporter.widgets.gdx_export_settings

######################################################################################################################
# Copyright (C) 2017-2020 Spine project consortium
# This file is part of Spine Toolbox.
# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
######################################################################################################################

"""
Export item's settings window for .gdx export.

:author: A. Soininen (VTT)
:date:   9.9.2019
"""

from copy import deepcopy
import enum
from PySide2.QtCore import QItemSelection, QModelIndex, Qt, Signal, Slot
from PySide2.QtWidgets import QAbstractButton, QDialogButtonBox, QMessageBox, QWidget
from spinedb_api import SpineDBAPIError
import spinetoolbox.spine_io.exporters.gdx as gdx
from ..db_utils import scenario_filtered_database_map
from ..list_utils import move_selected_elements_by
from ..mvcmodels.record_list_model import RecordListModel
from ..mvcmodels.set_list_model import SetListModel
from ..settings_state import SettingsState
from .parameter_index_settings_window import ParameterIndexSettingsWindow
from .parameter_merging_settings_window import ParameterMergingSettingsWindow


[docs]class State(enum.Enum): """Gdx Export Settings window state"""
[docs] OK = enum.auto()
"""Settings are ok."""
[docs] BAD_INDEXING = enum.auto()
"""Not all indexed parameters are set up correctly."""
[docs]class GdxExportSettings(QWidget): """A setting window for exporting .gdx files."""
[docs] reset_requested = Signal(str)
"""Emitted when Reset Defaults button has been clicked."""
[docs] settings_accepted = Signal(str)
"""Emitted when the OK button has been clicked."""
[docs] settings_rejected = Signal(str)
"""Emitted when the Cancel button has been clicked.""" def __init__( self, set_settings, indexing_settings, merging_settings, none_fallback, none_export, scenario, database_url, parent, ): """ Args: set_settings (gdx.SetSettings): export settings for GAMS sets indexing_settings (dict): indexing domain information for indexed parameter values merging_settings (dict): parameter merging settings none_fallback (NoneFallback): fallback for None parameter values none_export (NoneExport): how to handle None values while exporting scenario (str, optional): scenario name database_url (str): database URL parent (QWidget): a parent widget """ from ..ui.gdx_export_settings import Ui_Form # pylint: disable=import-outside-toplevel super().__init__(parent=parent, f=Qt.Window) self._ui = Ui_Form() self._ui.setupUi(self) self.setWindowTitle("Gdx Export settings -- {} --".format(database_url)) self.setAttribute(Qt.WA_DeleteOnClose, True) self._scenario = scenario self._database_url = database_url self._ui.button_box.accepted.connect(self._accept) self._ui.button_box.rejected.connect(self._reject) self._ui.button_box.clicked.connect(self._reset_settings) self._ui.button_box.button(QDialogButtonBox.RestoreDefaults).setToolTip( "Reset all settings\nby reloading the database." ) self._ui.set_move_up_button.clicked.connect(self._move_sets_up) self._ui.set_move_down_button.clicked.connect(self._move_sets_down) self._populate_global_parameters_combo_box(set_settings) self._ui.global_parameters_combo_box.currentIndexChanged[str].connect(self._update_global_parameters_domain) self._ui.record_sort_alphabetic.clicked.connect(self._sort_records_alphabetically) self._ui.record_move_up_button.clicked.connect(self._move_records_up) self._ui.record_move_down_button.clicked.connect(self._move_records_down) self._set_settings = set_settings self._set_list_model = SetListModel(set_settings,) self._set_list_model.dataChanged.connect(self._domains_sets_exportable_state_changed) self._ui.set_list_view.setModel(self._set_list_model) record_list_model = RecordListModel() self._ui.record_list_view.setModel(record_list_model) self._ui.set_list_view.selectionModel().selectionChanged.connect(self._populate_set_contents) self._ui.open_indexed_parameter_settings_button.clicked.connect(self._show_indexed_parameter_settings) self._ui.open_parameter_merging_settings_button.clicked.connect(self._show_parameter_merging_settings) self._indexing_settings = indexing_settings self._indexed_parameter_settings_window = None self._merging_settings = merging_settings self._parameter_merging_settings_window = None self._none_fallback = none_fallback self._none_export = none_export self._init_none_fallback_combo_box(none_fallback) self._init_none_export_combo_box(none_export) self._ui.none_fallback_combo_box.currentTextChanged.connect(self._set_none_fallback) self._ui.none_export_combo_box.currentTextChanged.connect(self._set_none_export) self._state = State.OK self._check_state() @property
[docs] def set_settings(self): """the settings object""" return self._set_settings
@property
[docs] def indexing_settings(self): """indexing settings dict""" return self._indexing_settings
@property
[docs] def merging_settings(self): """dictionary of merging settings""" return self._merging_settings
@property
[docs] def none_fallback(self): return self._none_fallback
@property
[docs] def none_export(self): return self._none_export
[docs] def reset_settings(self, set_settings, indexing_settings, merging_settings): """Resets all settings.""" if self._indexed_parameter_settings_window is not None: self._indexed_parameter_settings_window.close() self._indexed_parameter_settings_window = None if self._parameter_merging_settings_window is not None: self._parameter_merging_settings_window.close() self._parameter_merging_settings_window = None self._ui.global_parameters_combo_box.clear() self._populate_global_parameters_combo_box(set_settings) self._set_settings = set_settings self._set_list_model = SetListModel(set_settings) self._set_list_model.dataChanged.connect(self._domains_sets_exportable_state_changed) self._ui.set_list_view.setModel(self._set_list_model) self._ui.set_list_view.selectionModel().selectionChanged.connect(self._populate_set_contents) self._ui.record_list_view.setModel(RecordListModel()) self._indexing_settings = indexing_settings self._merging_settings = merging_settings self._check_state()
[docs] def _check_state(self): """Checks if there are parameters in need for indexing.""" for setting in self.indexing_settings.values(): if setting.indexing_domain_name is None and self._set_settings.is_exportable(setting.set_name): self._ui.indexing_status_label.setText( "<span style='color:#ff3333;white-space: pre-wrap;'>Not all parameters correctly indexed.</span>" ) self._state = State.BAD_INDEXING return self._state = State.OK self._ui.indexing_status_label.setText("")
@Slot(str)
[docs] def _set_none_fallback(self, option): """ Sets the None fallback option. Args: option (str): option as a label in the combo box """ if option == "Use it": self._none_fallback = gdx.NoneFallback.USE_IT else: self._none_fallback = gdx.NoneFallback.USE_DEFAULT_VALUE try: database_map = scenario_filtered_database_map(self._database_url, self._scenario) except SpineDBAPIError as error: QMessageBox.warning(self, f"Error", f"Could not open database '{self._database_url}'.") return try: indexing_settings = gdx.make_indexing_settings(database_map, self._none_fallback, logger=None) except gdx.GdxExportException as error: QMessageBox.warning( self, "Error", f"Failed to read indexing settings from database '{self._database_url}':\n{error}" ) return finally: database_map.connection.close() self._indexing_settings = gdx.update_indexing_settings( self._indexing_settings, indexing_settings, self._set_settings ) if self._indexed_parameter_settings_window is not None: self._indexed_parameter_settings_window.close() self._indexed_parameter_settings_window = None
[docs] def _init_none_fallback_combo_box(self, fallback): """ Sets the current text in None fallback combo box. Args: fallback (NoneFallback): option """ if fallback == gdx.NoneFallback.USE_IT: self._ui.none_fallback_combo_box.setCurrentText("Use it") else: self._ui.none_fallback_combo_box.setCurrentText("Replace by default value")
@Slot(str)
[docs] def _set_none_export(self, option): """ Sets the None export option. Args: option (str): option as a label in the combo box """ if option == "Do not export": self._none_export = gdx.NoneExport.DO_NOT_EXPORT else: self._none_export = gdx.NoneExport.EXPORT_AS_NAN
[docs] def _init_none_export_combo_box(self, export): """ Sets the current text in None export combo box Args: export (NoneExport): option """ if export == gdx.NoneExport.DO_NOT_EXPORT: self._ui.none_export_combo_box.setCurrentText("Do not export") else: self._ui.none_export_combo_box.setCurrentText("Export as not-a-number")
[docs] def _populate_global_parameters_combo_box(self, settings): """(Re)populates the global parameters combo box.""" self._ui.global_parameters_combo_box.addItem("Nothing selected") usable_domains = [name for name in settings.domain_names if not settings.metadata(name).is_additional()] self._ui.global_parameters_combo_box.addItems(usable_domains) if settings.global_parameters_domain_name: self._ui.global_parameters_combo_box.setCurrentText(settings.global_parameters_domain_name)
[docs] def _set_records_ordering_controls_enabled(self, enabled): """ Sets or unsets the enabled state of buttons that control the record key order. Args: enabled: True if the buttons should be enabled, False otherwise """ self._ui.record_sort_alphabetic.setEnabled(enabled) self._ui.record_move_down_button.setEnabled(enabled) self._ui.record_move_up_button.setEnabled(enabled)
@Slot(object)
[docs] def handle_settings_state_changed(self, state): enabled = state != SettingsState.FETCHING self._ui.set_group_box.setEnabled(enabled) self._ui.contents_group_box.setEnabled(enabled) self._ui.misc_control_holder.setEnabled(enabled) self._ui.button_box.button(QDialogButtonBox.Ok).setEnabled(enabled) self._ui.button_box.button(QDialogButtonBox.RestoreDefaults).setEnabled(enabled)
@Slot()
[docs] def _accept(self): """Emits the settings_accepted signal.""" if self._state != State.OK: QMessageBox.warning( self, "Bad Parameter Indexing", "Parameter indexing not set up correctly. Click 'Indexed parameters...' to open the settings window.", ) return self.settings_accepted.emit(self._database_url) self.hide()
@Slot(bool)
[docs] def _move_sets_up(self, checked=False): """Moves selected domains and sets up one position.""" move_selected_elements_by(self._ui.set_list_view, -1)
@Slot(bool)
[docs] def _move_sets_down(self, checked=False): """Moves selected domains and sets down one position.""" move_selected_elements_by(self._ui.set_list_view, 1)
@Slot(bool)
[docs] def _move_records_up(self, checked=False): """Moves selected records up and position.""" move_selected_elements_by(self._ui.record_list_view, -1)
@Slot(bool)
[docs] def _move_records_down(self, checked=False): """Moves selected records down on position.""" move_selected_elements_by(self._ui.record_list_view, 1)
@Slot()
[docs] def _reject(self): """Closes the window.""" self.close()
[docs] def closeEvent(self, event): super().closeEvent(event) self.settings_rejected.emit(self._database_url)
@Slot(QAbstractButton)
[docs] def _reset_settings(self, button): """Requests for fresh settings to be read from the database.""" if self._ui.button_box.standardButton(button) != QDialogButtonBox.RestoreDefaults: return self.reset_requested.emit(self._database_url)
@Slot(str)
[docs] def _update_global_parameters_domain(self, text): """Updates the global parameters domain name.""" if text == "Nothing selected": text = "" self._set_list_model.update_global_parameters_domain(text)
@Slot(QItemSelection, QItemSelection)
[docs] def _populate_set_contents(self, selected, _): """Populates the record list by the selected domain's or set's records.""" selected_indexes = selected.indexes() if not selected_indexes: return selected_set_name = self._set_list_model.data(selected_indexes[0]) records = self._set_settings.records(selected_set_name) record_model = self._ui.record_list_view.model() record_model.reset(records, selected_set_name) self._set_records_ordering_controls_enabled(records.is_shufflable())
@Slot(bool)
[docs] def _sort_records_alphabetically(self, _): """Sorts the lists of set records alphabetically.""" model = self._ui.record_list_view.model() model.sort_alphabetically()
@Slot(bool)
[docs] def _show_indexed_parameter_settings(self, _): """Shows the indexed parameter settings window.""" if self._indexed_parameter_settings_window is None: indexing_settings = deepcopy(self._indexing_settings) self._indexed_parameter_settings_window = ParameterIndexSettingsWindow( indexing_settings, self._set_settings, self._database_url, self._scenario, self ) self._indexed_parameter_settings_window.settings_approved.connect(self._gather_parameter_indexing_settings) self._indexed_parameter_settings_window.settings_rejected.connect( self._dispose_parameter_indexing_settings_window ) self._indexed_parameter_settings_window.show()
@Slot(bool)
[docs] def _show_parameter_merging_settings(self, _): """Shows the parameter merging settings window.""" if self._parameter_merging_settings_window is None: self._parameter_merging_settings_window = ParameterMergingSettingsWindow( self._merging_settings, self._database_url, self ) self._parameter_merging_settings_window.settings_approved.connect(self._parameter_merging_approved) self._parameter_merging_settings_window.settings_rejected.connect(self._dispose_parameter_merging_window) self._parameter_merging_settings_window.show()
@Slot()
[docs] def _gather_parameter_indexing_settings(self): """Gathers settings from the indexed parameters settings window.""" self._indexing_settings = self._indexed_parameter_settings_window.indexing_settings indexing_domains = self._indexed_parameter_settings_window.additional_indexing_domains() self._set_list_model.update_indexing_domains(indexing_domains) self._state = State.OK self._ui.indexing_status_label.setText("")
@Slot()
[docs] def _parameter_merging_approved(self): """Collects merging settings from the parameter merging window.""" new_merging_settings = self._parameter_merging_settings_window.merging_settings old_domain_names = {s.new_domain_name for s in self._merging_settings.values()} new_domain_names = {s.new_domain_name for s in new_merging_settings.values()} new_records = { setting.new_domain_name: gdx.merging_records(setting) for setting in new_merging_settings.values() } for domain_to_drop in old_domain_names - new_domain_names: self._set_list_model.drop_domain(domain_to_drop) for domain_to_update in old_domain_names & new_domain_names: self._set_list_model.update_domain(domain_to_update, new_records[domain_to_update]) for domain_to_add in new_domain_names - old_domain_names: self._set_list_model.add_domain(domain_to_add, new_records[domain_to_add], gdx.Origin.MERGING) self._merging_settings = new_merging_settings
@Slot()
[docs] def _dispose_parameter_indexing_settings_window(self): """Removes references to the indexed parameter settings window.""" self._indexed_parameter_settings_window = None
@Slot()
[docs] def _dispose_parameter_merging_window(self): """Removes references to the parameter merging settings window.""" self._parameter_merging_settings_window = None
@Slot(QModelIndex, QModelIndex, list)
[docs] def _domains_sets_exportable_state_changed(self, top_left, bottom_right, _): name = self._set_list_model.data(top_left) for setting in self._indexing_settings.values(): if name == setting.set_name: self._check_state() return