class ProviderToolBar(QToolBar): ''' Widget to display the dataprovider status ''' triggered = pyqtSignal(str) def __init__(self, parent=None): super(ProviderToolBar, self).__init__(parent) self.signalMapper = QSignalMapper(self) self.setMovable(True) self.setFloatable(True) self.actions = [] self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.signalMapper.mapped['QString'].connect(self.triggered) def createAction(self, provider): icon = QIcon(':/plugins/PosiView/ledgreen.png') icon.addFile(':/plugins/PosiView/ledgrey.png', QSize(), QIcon.Disabled, QIcon.Off) action = QAction(icon, provider.name, None) button = QToolButton() button.setDefaultAction(action) action.setEnabled(False) provider.deviceConnected.connect(action.setEnabled) provider.deviceDisconnected.connect(action.setDisabled) self.signalMapper.setMapping(action, provider.name) action.triggered.connect(self.signalMapper.map) self.addAction(action) self.actions.append(action)
class DocumentViewManager(QMainWindow): """ MDI area for displaying supporting documents within a given context e.g. supporting documents for a specific household based on the lifetime of the 'SourceDocumentManager' instance. """ def __init__(self, parent=None): QMainWindow.__init__(self, parent) self.setWindowFlags(Qt.Window) self._mdi_area = QMdiArea() self._mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self._mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setCentralWidget(self._mdi_area) # set the size of mid_area and DocumentViewManager based on the # screen size. screen = QDesktopWidget().availableGeometry() self._mdi_area.resize(screen.width() - 30, screen.height() - 80) self.resize(self._mdi_area.size()) self._mdi_area.subWindowActivated.connect(self.update_actions) self._viewer_mapper = QSignalMapper(self) self._viewer_mapper.mapped[QWidget].connect(self.set_active_sub_window) win_title = QApplication.translate("DocumentViewManager", "Document Viewer") self.setWindowTitle(win_title) self.setUnifiedTitleAndToolBarOnMac(True) self.statusBar().showMessage( QApplication.translate("DocumentViewManager", "Ready")) self._doc_viewers = {} self._create_menu_actions() self.update_actions() def center(self): """ Move the Document viewer to the center of the screen. """ # Get the current screens' dimensions... screen = QDesktopWidget().availableGeometry() # ... and get this windows' dimensions mdi_area_size = self.frameGeometry() # The horizontal position hpos = (screen.width() - mdi_area_size.width()) / 2 # vertical position vpos = (screen.height() - mdi_area_size.height()) / 2 # repositions the window self.move(hpos, vpos) def _create_menu_actions(self): self._window_menu = self.menuBar().addMenu( QApplication.translate("DocumentViewManager", "&Windows")) self._close_act = QAction( QApplication.translate("DocumentViewManager", "Cl&ose"), self) self._close_act.setStatusTip( QApplication.translate("DocumentViewManager", "Close the active document viewer")) self._close_act.triggered.connect(self._mdi_area.closeActiveSubWindow) self._close_all_act = QAction( QApplication.translate("DocumentViewManager", "Close &All"), self) self._close_all_act.setStatusTip( QApplication.translate("DocumentViewManager", "Close all the document viewers")) self._close_all_act.triggered.connect( self._mdi_area.closeAllSubWindows) self._tile_act = QAction( QApplication.translate("DocumentViewManager", "&Tile"), self) self._tile_act.setStatusTip( QApplication.translate("DocumentViewManager", "Tile the document viewers")) self._tile_act.triggered.connect(self.tile_windows) self._cascade_act = QAction( QApplication.translate("DocumentViewManager", "&Cascade"), self) self._cascade_act.setStatusTip( QApplication.translate("DocumentViewManager", "Cascade the document viewers")) self._cascade_act.triggered.connect(self.cascade_windows) self._next_act = QAction( QApplication.translate("DocumentViewManager", "Ne&xt"), self) self._next_act.setStatusTip( QApplication.translate( "DocumentViewManager", "Move the focus to the next document viewer")) self._next_act.triggered.connect(self._mdi_area.activateNextSubWindow) self._previous_act = QAction( QApplication.translate("DocumentViewManager", "Pre&vious"), self) self._previous_act.setStatusTip( QApplication.translate( "DocumentViewManager", "Move the focus to the previous document viewer")) self._previous_act.triggered.connect( self._mdi_area.activatePreviousSubWindow) self._separator_act = QAction(self) self._separator_act.setSeparator(True) self.update_window_menu() self._window_menu.aboutToShow.connect(self.update_window_menu) def cascade_windows(self): # Cascade document windows self._mdi_area.cascadeSubWindows() def tile_windows(self): # Arrange document windows to occupy the available space in mdi area self._mdi_area.tileSubWindows() def update_actions(self): if self._mdi_area.activeSubWindow(): has_mdi_child = True else: has_mdi_child = False self._close_act.setEnabled(has_mdi_child) self._close_all_act.setEnabled(has_mdi_child) self._tile_act.setEnabled(has_mdi_child) self._cascade_act.setEnabled(has_mdi_child) self._previous_act.setEnabled(has_mdi_child) self._next_act.setEnabled(has_mdi_child) self._separator_act.setVisible(has_mdi_child) def update_window_menu(self): self._window_menu.clear() self._window_menu.addAction(self._close_act) self._window_menu.addAction(self._close_all_act) self._window_menu.addSeparator() self._window_menu.addAction(self._tile_act) self._window_menu.addAction(self._cascade_act) self._window_menu.addSeparator() self._window_menu.addAction(self._next_act) self._window_menu.addAction(self._previous_act) self._window_menu.addAction(self._separator_act) windows = self._mdi_area.subWindowList() self._separator_act.setVisible(len(windows) != 0) for i, window in enumerate(windows): text = "%d. %s" % (i + 1, window.windowTitle()) win_action = self._window_menu.addAction(text) win_action.setCheckable(True) win_action.setChecked(window is self._mdi_area.activeSubWindow()) win_action.triggered.connect(self._viewer_mapper.map) self._viewer_mapper.setMapping(win_action, window) def load_viewer(self, document_widget, visible=True): """ Open a new instance of the viewer or activate an existing one if the document had been previously loaded. :param document_widget: Contains all the necessary information required to load the specific document. :type document_widget: DocumentWidget :param visible: True to show the view manager after the viewer has been loaded, otherwise it will be the responsibility of the caller to enable visibility. :type visible: bool :returns: True if the document was successfully loaded, else False. :rtype: bool """ doc_identifier = document_widget.file_identifier() if doc_identifier in self._doc_viewers: doc_sw = self._doc_viewers[doc_identifier] self._mdi_area.setActiveSubWindow(doc_sw) doc_sw.showNormal() else: abs_doc_path = self.absolute_document_path(document_widget) if not QFile.exists(abs_doc_path): msg = QApplication.translate( "DocumentViewManager", "The selected document does not exist." "\nPlease check the supporting documents' " "repository setting.") QMessageBox.critical( self, QApplication.translate("DocumentViewManager", "Invalid Document"), msg) return False file_info = QFileInfo(abs_doc_path) ext = file_info.suffix().lower() if ext == 'pdf': os.startfile(abs_doc_path) return True doc_viewer = self._create_viewer(document_widget) doc_viewer.load_document(abs_doc_path) self._doc_viewers[doc_identifier] = doc_viewer self._mdi_area.addSubWindow(doc_viewer) doc_viewer.show() if not self.isVisible() and visible: self.setVisible(True) if self.isMinimized(): self.showNormal() self.center() return True def set_active_sub_window(self, viewer): if viewer: self._mdi_area.setActiveSubWindow(viewer) def absolute_document_path(self, document_widget): """ Build the absolute document path using info from the document widget. :param document_widget: Instance of document widget. :return: Absolute path of the supporting document. :rtype: str """ abs_path = '' file_manager = document_widget.fileManager if not file_manager is None: network_repository = file_manager.networkPath file_id = document_widget.file_identifier() source_entity = document_widget.doc_source_entity() profile_name = current_profile().name doc_type = document_widget.doc_type_value().lower().replace( ' ', '_') file_name, file_extension = guess_extension( document_widget.displayName()) abs_path = network_repository + "/" + profile_name + '/' + \ str(source_entity) + "/" + str(doc_type) + "/" + \ str(file_id) + str(file_extension) return abs_path def reset(self): """ Removes all document viewers in the view area. The QCloseEvent sent to each sub-window will decrement the register. """ self._mdi_area.closeAllSubWindows() def _create_viewer(self, document_widget): """ Creates a new instance of a document viewer. :param document_widget: Contains all the necessary information required to load the specific document. :return: Document viewer object :rtype: DocumentViewer """ doc_viewer = DocumentViewer(self._mdi_area, document_widget.file_identifier()) doc_viewer.setAttribute(Qt.WA_DeleteOnClose) doc_viewer.setWindowTitle(document_widget.displayName()) # TODO: Incorporate logic for determining # TODO: viewer based on document type ph_viewer = PhotoViewer() # v_layout = QVBoxLayout() # v_layout.addWidget(ph_viewer) # doc_viewer.setLayout(v_layout) doc_viewer.set_view_widget(ph_viewer) doc_viewer.closed.connect(self._on_viewer_closed) return doc_viewer def remove_viewer(self, viewer_id): """ Close and remove the viewer with the specified viewer ID. """ if viewer_id in self._doc_viewers: viewer = self._doc_viewers[viewer_id] self._mdi_area.setActiveSubWindow(viewer) self._mdi_area.closeActiveSubWindow() self._on_viewer_closed(viewer_id) def _on_viewer_closed(self, file_id): """ Slot raised when a document viewer is closed. """ if file_id in self._doc_viewers: del self._doc_viewers[file_id]
class ImportData(WIDGET, BASE): def __init__(self, parent=None): QWizard.__init__(self, parent) self.setupUi(self) self.btnSrcUp.setIcon(GuiUtils.get_icon('up.png')) self.btnSrcDown.setIcon(GuiUtils.get_icon('down.png')) self.btn_add_translator.setIcon(GuiUtils.get_icon('add.png')) self.btn_edit_translator.setIcon(GuiUtils.get_icon('edit.png')) self.btn_delete_translator.setIcon(GuiUtils.get_icon('remove.png')) self.btnDestUp.setIcon(GuiUtils.get_icon('up.png')) self.btnDestDown.setIcon(GuiUtils.get_icon('down.png')) self.curr_profile = current_profile() # Connect signals self.btnBrowseSource.clicked.connect(self.setSourceFile) self.lstDestTables.itemClicked.connect(self.destSelectChanged) self.btnSrcUp.clicked.connect(self.srcItemUp) self.btnSrcDown.clicked.connect(self.srcItemDown) self.btnSrcAll.clicked.connect(self.checkSrcItems) self.btnSrcNone.clicked.connect(self.uncheckSrcItems) self.btnDestUp.clicked.connect(self.targetItemUp) self.btnDestDown.clicked.connect(self.targetItemDown) self.lstSrcFields.currentRowChanged[int].connect(self.sourceRowChanged) self.lstTargetFields.currentRowChanged[int].connect( self.destRowChanged) self.lstTargetFields.currentRowChanged[int].connect( self._enable_disable_trans_tools) self.chk_virtual.toggled.connect(self._on_load_virtual_columns) # Data Reader self.dataReader = None # Init self.registerFields() # Geometry columns self.geomcols = [] # Initialize value translators from definitions self._init_translators() # self._set_target_fields_stylesheet() def _init_translators(self): translator_menu = QMenu(self) self._trans_widget_mgr = TranslatorWidgetManager(self) self._trans_signal_mapper = QSignalMapper(self) for trans_name, config in ValueTranslatorConfig.translators.items(): trans_action = QAction('{}...'.format(trans_name), translator_menu) self._trans_signal_mapper.setMapping(trans_action, trans_name) trans_action.triggered.connect(self._trans_signal_mapper.map) translator_menu.addAction(trans_action) if len(translator_menu.actions()) == 0: self.btn_add_translator.setEnabled(False) else: self.btn_add_translator.setMenu(translator_menu) self._trans_signal_mapper.mapped[str].connect( self._load_translator_dialog) self.btn_edit_translator.setEnabled(False) self.btn_delete_translator.setEnabled(False) self.btn_edit_translator.clicked.connect(self._on_edit_translator) self.btn_delete_translator.clicked.connect(self._on_delete_translator) def _load_translator_dialog(self, config_key): """ Load translator dialog. """ dest_column = self._selected_destination_column() src_column = self._selected_source_column() if dest_column: # Check if there is an existing dialog in the manager trans_dlg = self._trans_widget_mgr.translator_widget(dest_column) if trans_dlg is None: trans_config = ValueTranslatorConfig.translators.get( config_key, None) # Safety precaution if trans_config is None: return try: trans_dlg = trans_config.create(self, self._source_columns(), self.targetTab, dest_column, src_column) except RuntimeError as re: QMessageBox.critical( self, QApplication.translate('ImportData', 'Value Translator'), str(re)) return self._handle_translator_dlg(dest_column, trans_dlg) def _handle_translator_dlg(self, key, dlg): if dlg.exec_() == QDialog.Accepted: self._trans_widget_mgr.add_widget(key, dlg) self._enable_disable_trans_tools() def _on_edit_translator(self): """ Slot to load the translator widget specific for the selected column for editing. """ dest_column = self._selected_destination_column() if dest_column: # Check if there is an existing dialog in the manager trans_dlg = self._trans_widget_mgr.translator_widget(dest_column) self._handle_translator_dlg(dest_column, trans_dlg) def _on_delete_translator(self): """ Slot for deleting the translator widget for the selected column. """ dest_column = self._selected_destination_column() self._delete_translator(dest_column) def _delete_translator(self, destination_column): if not destination_column: return res = self._trans_widget_mgr.remove_translator_widget( destination_column) self._enable_disable_trans_tools() def _enable_disable_trans_tools(self, index=-1): """ Enable/disable appropriate value translator tools based on the selected column. """ dest_column = self._selected_destination_column() if dest_column: # Check if there is an existing dialog in the manager trans_dlg = self._trans_widget_mgr.translator_widget(dest_column) if trans_dlg is None: self.btn_add_translator.setEnabled(True) self.btn_edit_translator.setEnabled(False) self.btn_delete_translator.setEnabled(False) else: self.btn_add_translator.setEnabled(False) self.btn_edit_translator.setEnabled(True) self.btn_delete_translator.setEnabled(True) else: self.btn_add_translator.setEnabled(False) self.btn_edit_translator.setEnabled(False) self.btn_delete_translator.setEnabled(False) def _selected_destination_column(self): dest_field_item = self.lstTargetFields.currentItem() if dest_field_item is None: return "" else: return dest_field_item.text() def _selected_source_column(self): src_field_item = self.lstSrcFields.currentItem() if src_field_item is None: return "" else: return src_field_item.text() def _set_target_fields_stylesheet(self): self.lstTargetFields.setStyleSheet( "QListWidget#lstTargetFields::item:selected" " { selection-background-color: darkblue }") def registerFields(self): # Register wizard fields pgSource = self.page(0) pgSource.registerField("srcFile*", self.txtDataSource) pgSource.registerField("typeText", self.rbTextType) pgSource.registerField("typeSpatial", self.rbSpType) # Destination table configuration destConf = self.page(1) destConf.registerField("optAppend", self.rbAppend) destConf.registerField("optOverwrite", self.rbOverwrite) destConf.registerField("tabIndex*", self.lstDestTables) destConf.registerField("geomCol", self.geomClm, "currentText", QComboBox.currentIndexChanged[int]) def initializePage(self, pageid): # Re-implementation of wizard page initialization if pageid == 1: # Reference to checked listwidget item representing table name self.destCheckedItem = None self.geomClm.clear() if self.field("typeText"): self.loadTables("textual") self.geomClm.setEnabled(False) elif self.field("typeSpatial"): self.loadTables("spatial") self.geomClm.setEnabled(True) if pageid == 2: self.lstSrcFields.clear() self.lstTargetFields.clear() self.assignCols() self._enable_disable_trans_tools() def _source_columns(self): return self.dataReader.getFields() def assignCols(self): # Load source and target columns respectively srcCols = self._source_columns() for c in srcCols: srcItem = QListWidgetItem(c, self.lstSrcFields) srcItem.setCheckState(Qt.Unchecked) srcItem.setIcon(GuiUtils.get_icon("column.png")) self.lstSrcFields.addItem(srcItem) # Destination Columns tabIndex = int(self.field("tabIndex")) self.targetTab = self.destCheckedItem.text() targetCols = table_column_names(self.targetTab, False, True) # Remove geometry columns in the target columns list for gc in self.geomcols: colIndex = getIndex(targetCols, gc) if colIndex != -1: targetCols.remove(gc) # Remove 'id' column if there id_idx = getIndex(targetCols, 'id') if id_idx != -1: targetCols.remove('id') self._add_target_table_columns(targetCols) def _add_target_table_columns(self, items, style=False): for item in items: list_item = QListWidgetItem(item) if style: color = QColor(0, 128, 255) list_item.setTextColor(color) self.lstTargetFields.addItem(list_item) def _on_load_virtual_columns(self, state): """ Load/unload relationships in the list of destination table columns. """ virtual_columns = self.dataReader.entity_virtual_columns( self.targetTab) if state: if len(virtual_columns) == 0: msg = QApplication.translate( "ImportData", "There are no virtual columns for the specified table.") QMessageBox.warning( self, QApplication.translate('ImportData', 'Import Data'), msg) self.chk_virtual.setChecked(False) return self._add_target_table_columns(virtual_columns, True) else: self._remove_destination_table_fields(virtual_columns) def _remove_destination_table_fields(self, fields): """Remove the specified columns from the destination view.""" for f in fields: list_items = self.lstTargetFields.findItems(f, Qt.MatchFixedString) if len(list_items) > 0: list_item = list_items[0] row = self.lstTargetFields.row(list_item) rem_item = self.lstTargetFields.takeItem(row) del rem_item # Delete translator if already defined for the given column self._delete_translator(f) def loadGeomCols(self, table): # Load geometry columns based on the selected table self.geomcols = table_column_names(table, True, True) self.geomClm.clear() self.geomClm.addItems(self.geomcols) def loadTables(self, type): # Load textual or spatial tables self.lstDestTables.clear() tables = None if type == "textual": tables = profile_user_tables(self.curr_profile, False, True) elif type == "spatial": tables = profile_spatial_tables(self.curr_profile) if tables is not None: for t in tables: tabItem = QListWidgetItem(t, self.lstDestTables) tabItem.setCheckState(Qt.Unchecked) tabItem.setIcon(GuiUtils.get_icon("table.png")) self.lstDestTables.addItem(tabItem) def validateCurrentPage(self): # Validate the current page before proceeding to the next one validPage = True if not QFile.exists(str(self.field("srcFile"))): self.ErrorInfoMessage("The specified source file does not exist.") validPage = False else: if self.dataReader: self.dataReader.reset() self.dataReader = OGRReader(str(self.field("srcFile"))) if not self.dataReader.isValid(): self.ErrorInfoMessage("The source file could not be opened." "\nPlease check is the given file type " "is supported") validPage = False if self.currentId() == 1: if self.destCheckedItem == None: self.ErrorInfoMessage("Please select the destination table.") validPage = False if self.currentId() == 2: validPage = self.execImport() return validPage def setSourceFile(self): # Set the file path to the source file imageFilters = "Comma Separated Value (*.csv);;ESRI Shapefile (*.shp);;AutoCAD DXF (*.dxf)" sourceFile, _ = QFileDialog.getOpenFileName(self, "Select Source File", vectorFileDir(), imageFilters) if sourceFile: self.txtDataSource.setText(sourceFile) def getSrcDestPairs(self): # Return the matched source and destination columns srcDest = {} for l in range(self.lstTargetFields.count()): if l < self.lstSrcFields.count(): srcItem = self.lstSrcFields.item(l) if srcItem.checkState() == Qt.Checked: destItem = self.lstTargetFields.item(l) srcDest[srcItem.text()] = destItem.text() return srcDest def execImport(self): # Initiate the import process success = False matchCols = self.getSrcDestPairs() # Specify geometry column geom_column = None if self.field("typeSpatial"): geom_column = self.field("geomCol") # Ensure that user has selected at least one column if it is a # non-spatial table if len(matchCols) == 0: self.ErrorInfoMessage("Please select at least one source column.") return success value_translator_manager = self._trans_widget_mgr.translator_manager() try: if self.field("optOverwrite"): entity = self.curr_profile.entity_by_name(self.targetTab) dependencies = entity.dependencies() view_dep = dependencies['views'] entity_dep = [e.name for e in entity.children()] entities_dep_str = ', '.join(entity_dep) views_dep_str = ', '.join(view_dep) if len(entity_dep) > 0 or len(view_dep) > 0: del_msg = QApplication.translate( 'ImportData', "Overwriting existing records will permanently \n" "remove records from other tables linked to the \n" "records. The following tables will be affected." "\n{}\n{}" "\nClick Yes to proceed importing or No to cancel.". format(entities_dep_str, views_dep_str)) del_result = QMessageBox.critical( self, QApplication.translate( "ImportData", "Overwrite Import Data Warning"), del_msg, QMessageBox.Yes | QMessageBox.No) if del_result == QMessageBox.Yes: self.dataReader.featToDb( self.targetTab, matchCols, False, self, geom_column, translator_manager=value_translator_manager) # Update directory info in the registry setVectorFileDir(self.field("srcFile")) self.InfoMessage( "All features have been imported successfully!") success = True else: success = False else: self.dataReader.featToDb( self.targetTab, matchCols, True, self, geom_column, translator_manager=value_translator_manager) self.InfoMessage( "All features have been imported successfully!") # Update directory info in the registry setVectorFileDir(self.field("srcFile")) success = True except: self.ErrorInfoMessage(str(sys.exc_info()[1])) return success def _clear_dest_table_selections(self, exclude=None): # Clears checked items in destination table list view if exclude is None: exclude = [] for i in range(self.lstDestTables.count()): item = self.lstDestTables.item(i) if item.checkState() == Qt.Checked and not item.text() in exclude: item.setCheckState(Qt.Unchecked) def destSelectChanged(self, item): """ Handler when a list widget item is clicked, clears previous selections """ if not self.destCheckedItem is None: if item.checkState() == Qt.Checked: self.destCheckedItem.setCheckState(Qt.Unchecked) else: self.destCheckedItem = None if item.checkState() == Qt.Checked: self.destCheckedItem = item # Ensure other selected items have been cleared self._clear_dest_table_selections(exclude=[item.text()]) # Load geometry columns if selection is a spatial table if self.field("typeSpatial"): self.loadGeomCols(item.text()) def syncRowSelection(self, srcList, destList): """ Sync the selection of an srcList item to the corresponding one in the destination column list. """ if (srcList.currentRow() + 1) <= destList.count(): destList.setCurrentRow(srcList.currentRow()) def sourceRowChanged(self): # Slot when the source list's current row changes self.syncRowSelection(self.lstSrcFields, self.lstTargetFields) def destRowChanged(self): # Slot when the destination list's current row changes self.syncRowSelection(self.lstTargetFields, self.lstSrcFields) def itemUp(self, listWidget): # Moves the selected item in the list widget one level up curIndex = listWidget.currentRow() curItem = listWidget.takeItem(curIndex) listWidget.insertItem(curIndex - 1, curItem) listWidget.setCurrentRow(curIndex - 1) def itemDown(self, listWidget): # Moves the selected item in the list widget one level down curIndex = listWidget.currentRow() curItem = listWidget.takeItem(curIndex) listWidget.insertItem(curIndex + 1, curItem) listWidget.setCurrentRow(curIndex + 1) def checkAllItems(self, listWidget, state): # Checks all items in the list widget for l in range(listWidget.count()): item = listWidget.item(l) if state: item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) def checkSrcItems(self): # Slot for checking all source table columns self.checkAllItems(self.lstSrcFields, True) def uncheckSrcItems(self): # Slot for unchecking all source table columns self.checkAllItems(self.lstSrcFields, False) def srcItemUp(self): # Slot for moving source list item up self.itemUp(self.lstSrcFields) def srcItemDown(self): # Slot for moving source list item down self.itemDown(self.lstSrcFields) def targetItemUp(self): # Slot for moving target item up self.itemUp(self.lstTargetFields) def targetItemDown(self): # Slot for moving target item down self.itemDown(self.lstTargetFields) def keyPressEvent(self, e): """ Override method for preventing the dialog from closing itself when the escape key is hit """ if e.key() == Qt.Key_Escape: pass def InfoMessage(self, message): # Information message box msg = QMessageBox() msg.setIcon(QMessageBox.Information) msg.setText(message) msg.exec_() def ErrorInfoMessage(self, message): # Error Message Box msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText(message) msg.exec_()
class ImportData(WIDGET, BASE): ROLE_TABLE_NAME = Qt.UserRole + 1 def __init__(self, parent=None): QWizard.__init__(self, parent) self.setupUi(self) self.btnSrcUp.setIcon(GuiUtils.get_icon('up.png')) self.btnSrcDown.setIcon(GuiUtils.get_icon('down.png')) self.btn_add_translator.setIcon(GuiUtils.get_icon('add.png')) self.btn_edit_translator.setIcon(GuiUtils.get_icon('edit.png')) self.btn_delete_translator.setIcon(GuiUtils.get_icon('remove.png')) self.btnDestUp.setIcon(GuiUtils.get_icon('up.png')) self.btnDestDown.setIcon(GuiUtils.get_icon('down.png')) self.curr_profile = current_profile() # Connect signals self.btnBrowseSource.clicked.connect(self.setSourceFile) self.lstDestTables.itemClicked.connect(self.destSelectChanged) self.btnSrcUp.clicked.connect(self.srcItemUp) self.btnSrcDown.clicked.connect(self.srcItemDown) self.btnSrcAll.clicked.connect(self.checkSrcItems) self.btnSrcNone.clicked.connect(self.uncheckSrcItems) self.btnDestUp.clicked.connect(self.targetItemUp) self.btnDestDown.clicked.connect(self.targetItemDown) self.lstSrcFields.currentRowChanged[int].connect(self.sourceRowChanged) self.lstTargetFields.currentRowChanged[int].connect(self.destRowChanged) self.lstTargetFields.currentRowChanged[int].connect(self._enable_disable_trans_tools) self.chk_virtual.toggled.connect(self._on_load_virtual_columns) self.button_save_configuration.clicked.connect(self._save_column_mapping) self.button_load_configuration.clicked.connect(self._load_column_mapping) self.targetTab = '' self.import_was_successful = False self.restored_config = {} # Data Reader self.dataReader = None # Init self.registerFields() # Geometry columns self.geom_cols = [] # Initialize value translators from definitions self._init_translators() # self._set_target_fields_stylesheet() if os.path.exists(BACKUP_IMPORT_CONFIG_PATH): self._restore_previous_configuration() def closeEvent(self, event): self._save_if_unfinished() super().closeEvent(event) def reject(self): self._save_if_unfinished() super().reject() def accept(self): super().accept() self._save_if_unfinished() def _save_if_unfinished(self): """ If an unfinished import is in progress, then automatically save the settings to a hidden file """ if self.import_was_successful: return if not self.field("srcFile"): # user hasn't even started the process by picking a file, so we've nothing of value to save... return current_config = self._get_column_config() with open(BACKUP_IMPORT_CONFIG_PATH, 'wt') as f: f.write(json.dumps(current_config, indent=4)) def _restore_previous_configuration(self): """ Loads the previously unfinished configuration """ with open(BACKUP_IMPORT_CONFIG_PATH, 'rt') as f: config = json.loads(''.join(f.readlines())) os.remove(BACKUP_IMPORT_CONFIG_PATH) if not config: return if QMessageBox.question(self, self.tr('Import Data'), self.tr( 'A previously incomplete or unsuccessful import was detected. Would you like to restore the previous configuration and retry?'), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes ) != QMessageBox.Yes: return self.restored_config = config self.txtDataSource.setText(self.restored_config.get('source_file')) self.rbTextType.setChecked(self.restored_config.get('is_text', True)) self.rbSpType.setChecked(self.restored_config.get('is_spatial', False)) def _init_translators(self): translator_menu = QMenu(self) self._trans_widget_mgr = TranslatorWidgetManager(self) self._trans_signal_mapper = QSignalMapper(self) for trans_name, config in ValueTranslatorConfig.translators.items(): trans_action = QAction('{}...'.format(trans_name), translator_menu ) self._trans_signal_mapper.setMapping(trans_action, trans_name) trans_action.triggered.connect(self._trans_signal_mapper.map) translator_menu.addAction(trans_action) if len(translator_menu.actions()) == 0: self.btn_add_translator.setEnabled(False) else: self.btn_add_translator.setMenu(translator_menu) self._trans_signal_mapper.mapped[str].connect(self._load_translator_dialog) self.btn_edit_translator.setEnabled(False) self.btn_delete_translator.setEnabled(False) self.btn_edit_translator.clicked.connect(self._on_edit_translator) self.btn_delete_translator.clicked.connect(self._on_delete_translator) def _load_translator_dialog(self, config_key): """ Load translator dialog. """ dest_column = self._selected_destination_column() src_column = self._selected_source_column() if dest_column: # Check if there is an existing dialog in the manager trans_dlg = self._trans_widget_mgr.translator_widget(dest_column) if trans_dlg is None: trans_config = ValueTranslatorConfig.translators.get(config_key, None) # Safety precaution if trans_config is None: return try: trans_dlg = trans_config.create( self, self._source_columns(), self.targetTab, dest_column, src_column ) except RuntimeError as re: QMessageBox.critical( self, QApplication.translate( 'ImportData', 'Value Translator' ), str(re) ) return self._handle_translator_dlg(dest_column, trans_dlg) def _handle_translator_dlg(self, key, dlg): if dlg.exec_() == QDialog.Accepted: self._trans_widget_mgr.add_widget(key, dlg) self._enable_disable_trans_tools() def _on_edit_translator(self): """ Slot to load the translator widget specific for the selected column for editing. """ dest_column = self._selected_destination_column() if dest_column: # Check if there is an existing dialog in the manager trans_dlg = self._trans_widget_mgr.translator_widget(dest_column) self._handle_translator_dlg(dest_column, trans_dlg) def _on_delete_translator(self): """ Slot for deleting the translator widget for the selected column. """ dest_column = self._selected_destination_column() self._delete_translator(dest_column) def _delete_translator(self, destination_column): if not destination_column: return _ = self._trans_widget_mgr.remove_translator_widget(destination_column) self._enable_disable_trans_tools() def _enable_disable_trans_tools(self, index=-1): """ Enable/disable appropriate value translator tools based on the selected column. """ dest_column = self._selected_destination_column() if dest_column: # Check if there is an existing dialog in the manager trans_dlg = self._trans_widget_mgr.translator_widget(dest_column) if trans_dlg is None: self.btn_add_translator.setEnabled(True) self.btn_edit_translator.setEnabled(False) self.btn_delete_translator.setEnabled(False) else: self.btn_add_translator.setEnabled(False) self.btn_edit_translator.setEnabled(True) self.btn_delete_translator.setEnabled(True) else: self.btn_add_translator.setEnabled(False) self.btn_edit_translator.setEnabled(False) self.btn_delete_translator.setEnabled(False) def _selected_destination_column(self): dest_field_item = self.lstTargetFields.currentItem() if dest_field_item is None: return "" else: return dest_field_item.text() def _selected_source_column(self): src_field_item = self.lstSrcFields.currentItem() if src_field_item is None: return "" else: return src_field_item.text() def _set_target_fields_stylesheet(self): self.lstTargetFields.setStyleSheet("QListWidget#lstTargetFields::item:selected" " { selection-background-color: darkblue }") def registerFields(self): # Register wizard fields pgSource = self.page(0) pgSource.registerField("srcFile*", self.txtDataSource) pgSource.registerField("typeText", self.rbTextType) pgSource.registerField("typeSpatial", self.rbSpType) # Destination table configuration destConf = self.page(1) destConf.registerField("optAppend", self.rbAppend) destConf.registerField("optOverwrite", self.rbOverwrite) destConf.registerField("tabIndex*", self.lstDestTables) destConf.registerField("geomCol", self.geomClm, "currentText", QComboBox.currentIndexChanged[int]) def initializePage(self, page_id): # Re-implementation of wizard page initialization if page_id == 1: # Reference to checked list widget item representing table name self.geomClm.clear() if self.field("typeText"): self.load_tables_of_type("textual", self.restored_config.get('dest_table')) self.geomClm.setEnabled(False) elif self.field("typeSpatial"): self.load_tables_of_type("spatial", self.restored_config.get('dest_table')) if self.selected_destination_table(): self.loadGeomCols(self.selected_destination_table()) self.geomClm.setEnabled(True) if self.restored_config: if self.restored_config.get('overwrite', False): self.rbOverwrite.setChecked(True) else: self.rbAppend.setChecked(True) if page_id == 2: self.lstSrcFields.clear() self.lstTargetFields.clear() self.assignCols() if self.restored_config and self.selected_destination_table() == self.restored_config.get('dest_table'): self._restore_column_config(self.restored_config) self._enable_disable_trans_tools() def _source_columns(self): return self.dataReader.getFields() @staticmethod def format_name_for_matching(name: str) -> str: """ Returns a column name formatted for tolerant matching, e.g. we ignore case, _ characters, etc """ return name.strip().lower().replace(' ', '').replace('_', '').replace('-', '') @staticmethod def names_are_matching(name1: str, name2: str) -> bool: """ Returns True if the specified column name pairs should be considered a tolerant match """ return ImportData.format_name_for_matching(name1) == ImportData.format_name_for_matching(name2) def selected_destination_table(self) -> Optional[str]: """ Returns the selected (checked) destination table """ for i in range(self.lstDestTables.count()): item = self.lstDestTables.item(i) if item.checkState() == Qt.Checked: return item.data(ImportData.ROLE_TABLE_NAME) return None def assignCols(self): # Load source and target columns respectively source_columns = self._source_columns() # Destination Columns self.targetTab = self.selected_destination_table() target_columns = table_column_names(self.targetTab, False, True) # Remove geometry columns and 'id' column in the target columns list target_columns = [c for c in target_columns if c not in self.geom_cols and c != 'id'] # now synchronize the lists, as much as possible # this consists of moving columns with matching names in the source and target lists to the same # placement at the top of the lists, and filtering out lists of remaining unmatched columns matched_source_columns = [] unmatched_source_columns = source_columns[:] matched_target_columns = [] unmatched_target_columns = target_columns[:] for source in source_columns: for target in unmatched_target_columns: if ImportData.names_are_matching(source, target): matched_source_columns.append(source) unmatched_source_columns = [c for c in unmatched_source_columns if c != source] matched_target_columns.append(target) unmatched_target_columns = [c for c in unmatched_target_columns if c != target] break # any matching columns get added to the start of the lists, and unmatched get added # to the end of the list for c in matched_source_columns + unmatched_source_columns: src_item = QListWidgetItem(c, self.lstSrcFields) # automatically check any columns we could match src_item.setCheckState(Qt.Checked if c in matched_source_columns else Qt.Unchecked) src_item.setIcon(GuiUtils.get_icon("column.png")) self.lstSrcFields.addItem(src_item) self._add_target_table_columns(matched_target_columns + unmatched_target_columns) def _add_target_table_columns(self, items, style=False): for item in items: list_item = QListWidgetItem(item) if style: color = QColor(0, 128, 255) list_item.setTextColor(color) self.lstTargetFields.addItem(list_item) def _on_load_virtual_columns(self, state): """ Load/unload relationships in the list of destination table columns. """ virtual_columns = self.dataReader.entity_virtual_columns(self.targetTab) if state: if len(virtual_columns) == 0: msg = QApplication.translate("ImportData", "There are no virtual columns for the specified table.") QMessageBox.warning( self, QApplication.translate( 'ImportData', 'Import Data' ), msg ) self.chk_virtual.setChecked(False) return self._add_target_table_columns(virtual_columns, True) else: self._remove_destination_table_fields(virtual_columns) def _remove_destination_table_fields(self, fields): """Remove the specified columns from the destination view.""" for f in fields: list_items = self.lstTargetFields.findItems(f, Qt.MatchFixedString) if len(list_items) > 0: list_item = list_items[0] row = self.lstTargetFields.row(list_item) rem_item = self.lstTargetFields.takeItem(row) del rem_item # Delete translator if already defined for the given column self._delete_translator(f) def loadGeomCols(self, table): # Load geometry columns based on the selected table self.geom_cols = table_column_names(table, True, True) self.geomClm.clear() self.geomClm.addItems(self.geom_cols) if self.restored_config.get('geom_column'): self.geomClm.setCurrentIndex(self.geomClm.findText(self.restored_config['geom_column'])) def load_tables_of_type(self, type: str, initial_selection: Optional[str] = None): """ Load textual or spatial tables If initial_selection is specified then that table will be initially checked """ self.lstDestTables.clear() tables = None if type == "textual": tables = profile_user_tables(self.curr_profile, False, True, include_read_only=False) elif type == "spatial": tables = profile_spatial_tables(self.curr_profile, include_read_only=False) if tables is not None: for table_name, table_label in tables.items(): table_item = QListWidgetItem(table_label, self.lstDestTables) table_item.setData(ImportData.ROLE_TABLE_NAME, table_name) if initial_selection: table_item.setCheckState( Qt.Checked if ImportData.names_are_matching(table_name, initial_selection) else Qt.Unchecked) else: table_item.setCheckState(Qt.Unchecked) table_item.setIcon(GuiUtils.get_icon("table.png")) self.lstDestTables.addItem(table_item) def validateCurrentPage(self): # Validate the current page before proceeding to the next one validPage = True if not QFile.exists(str(self.field("srcFile"))): self.show_error_message("The specified source file does not exist.") validPage = False else: if self.dataReader: self.dataReader.reset() self.dataReader = OGRReader(str(self.field("srcFile"))) if not self.dataReader.isValid(): self.show_error_message("The source file could not be opened." "\nPlease check is the given file type " "is supported") validPage = False if self.currentId() == 1: if self.selected_destination_table() is None: self.show_error_message("Please select the destination table.") validPage = False if self.currentId() == 2: self.import_was_successful = self.execImport() validPage = self.import_was_successful return validPage def setSourceFile(self): # Set the file path to the source file filters = "Comma Separated Value (*.csv);;ESRI Shapefile (*.shp);;AutoCAD DXF (*.dxf)" sourceFile, _ = QFileDialog.getOpenFileName(self, "Select Source File", vectorFileDir(), filters) if sourceFile: self.txtDataSource.setText(sourceFile) def get_source_dest_pairs(self) -> dict: """ Builds a dictionary of source field to destination field name """ mapping = {} for target_row in range(self.lstTargetFields.count()): if target_row < self.lstSrcFields.count(): source_item = self.lstSrcFields.item(target_row) if source_item.checkState() == Qt.Checked: dest_item = self.lstTargetFields.item(target_row) mapping[source_item.text()] = dest_item.text() return mapping def set_source_dest_pairs(self, mapping: dict): """ Sets the source to destination pairs for fields to match Any existing mapping will be cleared """ self.uncheckSrcItems() index = 0 for target_source, target_dest in mapping.items(): # move source row up for source_row in range(self.lstSrcFields.count()): if ImportData.names_are_matching(target_source, self.lstSrcFields.item(source_row).text()): source_item = self.lstSrcFields.takeItem(source_row) self.lstSrcFields.insertItem(index, source_item) source_item.setCheckState(Qt.Checked) # move target row up for dest_row in range(self.lstTargetFields.count()): if ImportData.names_are_matching(target_dest, self.lstTargetFields.item(dest_row).text()): dest_item = self.lstTargetFields.takeItem(dest_row) self.lstTargetFields.insertItem(index, dest_item) index += 1 def execImport(self): # Initiate the import process success = False matchCols = self.get_source_dest_pairs() # Specify geometry column geom_column = None if self.field("typeSpatial"): geom_column = self.field("geomCol") # Ensure that user has selected at least one column if it is a # non-spatial table if len(matchCols) == 0: self.show_error_message("Please select at least one source column.") return success value_translator_manager = self._trans_widget_mgr.translator_manager() try: if self.field("optOverwrite"): entity = self.curr_profile.entity_by_name(self.targetTab) dependencies = entity.dependencies() view_dep = dependencies['views'] entity_dep = [e.name for e in entity.children()] entities_dep_str = ', '.join(entity_dep) views_dep_str = ', '.join(view_dep) if len(entity_dep) > 0 or len(view_dep) > 0: del_msg = QApplication.translate( 'ImportData', "Overwriting existing records will permanently \n" "remove records from other tables linked to the \n" "records. The following tables will be affected." "\n{}\n{}" "\nClick Yes to proceed importing or No to cancel.". format(entities_dep_str, views_dep_str) ) del_result = QMessageBox.critical( self, QApplication.translate( "ImportData", "Overwrite Import Data Warning" ), del_msg, QMessageBox.Yes | QMessageBox.No ) if del_result == QMessageBox.Yes: self.dataReader.featToDb( self.targetTab, matchCols, False, self, geom_column, translator_manager=value_translator_manager ) # Update directory info in the registry setVectorFileDir(self.field("srcFile")) self.show_info_message( "All features have been imported successfully!" ) success = True else: success = False else: self.dataReader.featToDb( self.targetTab, matchCols, True, self, geom_column, translator_manager=value_translator_manager ) self.show_info_message( "All features have been imported successfully!" ) # Update directory info in the registry setVectorFileDir(self.field("srcFile")) success = True except ImportFeatureException as e: self.show_error_message(str(e)) return success def _clear_dest_table_selections(self, exclude=None): # Clears checked items in destination table list view if exclude is None: exclude = [] for i in range(self.lstDestTables.count()): item = self.lstDestTables.item(i) if item.checkState() == Qt.Checked and not item.data(ImportData.ROLE_TABLE_NAME) in exclude: item.setCheckState(Qt.Unchecked) def destSelectChanged(self, item): """ Handler when a list widget item is clicked, clears previous selections """ if item.checkState() == Qt.Checked: selected_table = item.data(ImportData.ROLE_TABLE_NAME) # Ensure other selected items have been cleared self._clear_dest_table_selections(exclude=[selected_table]) # Load geometry columns if selection is a spatial table if self.field("typeSpatial"): self.loadGeomCols(selected_table) def syncRowSelection(self, srcList, destList): """ Sync the selection of an srcList item to the corresponding one in the destination column list. """ if (srcList.currentRow() + 1) <= destList.count(): destList.setCurrentRow(srcList.currentRow()) def sourceRowChanged(self): # Slot when the source list's current row changes self.syncRowSelection(self.lstSrcFields, self.lstTargetFields) def destRowChanged(self): # Slot when the destination list's current row changes self.syncRowSelection(self.lstTargetFields, self.lstSrcFields) def itemUp(self, listWidget): # Moves the selected item in the list widget one level up curIndex = listWidget.currentRow() curItem = listWidget.takeItem(curIndex) listWidget.insertItem(curIndex - 1, curItem) listWidget.setCurrentRow(curIndex - 1) def itemDown(self, listWidget): # Moves the selected item in the list widget one level down curIndex = listWidget.currentRow() curItem = listWidget.takeItem(curIndex) listWidget.insertItem(curIndex + 1, curItem) listWidget.setCurrentRow(curIndex + 1) def checkAllItems(self, listWidget, state): # Checks all items in the list widget for l in range(listWidget.count()): item = listWidget.item(l) if state: item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) def checkSrcItems(self): # Slot for checking all source table columns self.checkAllItems(self.lstSrcFields, True) def uncheckSrcItems(self): # Slot for unchecking all source table columns self.checkAllItems(self.lstSrcFields, False) def srcItemUp(self): # Slot for moving source list item up self.itemUp(self.lstSrcFields) def srcItemDown(self): # Slot for moving source list item down self.itemDown(self.lstSrcFields) def targetItemUp(self): # Slot for moving target item up self.itemUp(self.lstTargetFields) def targetItemDown(self): # Slot for moving target item down self.itemDown(self.lstTargetFields) def keyPressEvent(self, e): """ Override method for preventing the dialog from closing itself when the escape key is hit """ if e.key() == Qt.Key_Escape: pass def show_info_message(self, message): # Information message box msg = QMessageBox() msg.setIcon(QMessageBox.Information) msg.setText(message) msg.exec_() def show_error_message(self, message): # Error Message Box msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText(message) msg.exec_() def _save_column_mapping(self): """ Exports the current column mapping to a JSON definition file """ config = RegistryConfig() prev_folder = config.read(["LastImportConfigFolder"]).get("LastImportConfigFolder") if not prev_folder: prev_folder = QDir.homePath() dest_path, _ = QFileDialog.getSaveFileName(self, self.tr("Save Configuration"), prev_folder, "{0} (*.json)".format(self.tr('Configuration files'))) if not dest_path: return dest_path = QgsFileUtils.ensureFileNameHasExtension(dest_path, ['.json']) config.write({"LastImportConfigFolder": QFileInfo(dest_path).path()}) with open(dest_path, 'wt') as f: f.write(json.dumps(self._get_column_config(), indent=4)) def _get_column_config(self) -> dict: """ Returns a dictionary encapsulating the column mapping configuration """ return { 'column_mapping': self.get_source_dest_pairs(), 'source_file': str(self.field("srcFile")), 'is_text': bool(self.field("typeText")), 'is_spatial': bool(self.field("typeSpatial")), 'geom_column': self.field("geomCol") or None, 'overwrite': bool(self.field("optOverwrite")), 'dest_table': self.targetTab or '' } def _load_column_mapping(self): """ Imports the current column mapping from a JSON definition file """ config = RegistryConfig() prev_folder = config.read(["LastImportConfigFolder"]).get("LastImportConfigFolder") if not prev_folder: prev_folder = QDir.homePath() source_path, _ = QFileDialog.getOpenFileName(self, self.tr("Load Configuration"), prev_folder, "{0} (*.json)".format(self.tr('Configuration files'))) if not source_path: return config.write({"LastImportConfigFolder": QFileInfo(source_path).path()}) with open(source_path, 'rt') as f: imported_config = json.loads(''.join(f.readlines())) self._restore_column_config(imported_config) def _restore_column_config(self, config: dict): """ Restores a previously saved column configuration """ column_mapping = config.get('column_mapping', {}) # test validity -- ensure that all the referenced source and destination columns # from the saved file are available for saved_source, saved_dest in column_mapping.items(): for source_row in range(self.lstSrcFields.count()): if ImportData.names_are_matching(saved_source, self.lstSrcFields.item(source_row).text()): break else: self.show_error_message(self.tr('Source column {} not found in dataset'.format(saved_source))) return for destination_row in range(self.lstTargetFields.count()): if ImportData.names_are_matching(saved_dest, self.lstTargetFields.item(destination_row).text()): break else: self.show_error_message(self.tr('Destination column {} not found in dataset'.format(saved_dest))) return self.set_source_dest_pairs(column_mapping)