class QualityRuleEngine(QObject): """ Engine that executes Quality Rules """ def __init__(self, db, rules): QObject.__init__(self) self.logger = Logger() self.app = AppInterface() self.__tolerance = self.app.settings.tolerance self.__layer_manager = QualityRuleLayerManager(db, rules.keys(), self.__tolerance) self.__quality_rules = QualityRules() self.quality_rule_logger = QualityRuleLogger(self.__tolerance) self.__db = db self.__rules = rules self.__result_layers = list() def initialize(self, db, rules): """ Objects of this class are reusable calling initialize() """ self.__result_layers = list() self.__db = db self.__rules = rules self.__tolerance = self.app.settings.tolerance self.__layer_manager.initialize(rules.keys(), self.__tolerance) self.quality_rule_logger.initialize(self.__tolerance) def validate_quality_rules(self): if self.__rules: self.quality_rule_logger.set_count_topology_rules(len(self.__rules)) self.logger.info(__name__, QCoreApplication.translate("QualityRuleEngine", "Validating {} quality rules (tolerance: {}).").format(len(self.__rules), self.__tolerance)) for rule_key, rule_name in self.__rules.items(): layers = self.__layer_manager.get_layers(rule_key) if layers: self.__validate_quality_rule(rule_key, layers, rule_name=rule_name) else: self.logger.warning(__name__, QCoreApplication.translate("QualityRuleEngine", "Couldn't execute '{}' quality rule! Required layers are not available. Skipping...").format(rule_name)) self.quality_rule_logger.generate_log_button() self.__layer_manager.clean_temporary_layers() else: self.logger.warning(__name__, QCoreApplication.translate("QualityRuleEngine", "No rules to validate!")) @_log_quality_rule_validations def __validate_quality_rule(self, rule_key, layers, rule_name): """ Intermediate function to log quality rule execution. :param rule_key: rule key :param rule_name: Rule name (needed for the logging decorator) :return: tuple (msg, level), where level indicates whether the rule was successful, couldn't be validated (warning), or was not successful (critical) """ return self.__quality_rules.validate_quality_rule(self.__db, rule_key, layers)
class WizardMessagesManager: def __init__(self, wizard_tool_name, editing_layer_name): self.__WIZARD_TOOL_NAME = wizard_tool_name self.__logger = Logger() self.__editing_layer_name = editing_layer_name def show_wizard_closed_msg(self): message = QCoreApplication.translate( "WizardTranslations", "'{}' tool has been closed.").format(self.__WIZARD_TOOL_NAME) self.__logger.info_msg(__name__, message) def show_form_closed_msg(self): message = QCoreApplication.translate( "WizardTranslations", "'{}' tool has been closed because you just closed the form.")\ .format(self.__WIZARD_TOOL_NAME) self.__logger.info_msg(__name__, message) def show_map_tool_changed_msg(self): message = QCoreApplication.translate( "WizardTranslations", "'{}' tool has been closed because the map tool change.")\ .format(self.__WIZARD_TOOL_NAME) self.__logger.info_msg(__name__, message) def show_layer_removed_msg(self): message = QCoreApplication.translate( "WizardTranslations", "'{}' tool has been closed because you just removed a required layer.")\ .format(self.__WIZARD_TOOL_NAME) self.__logger.info_msg(__name__, message) def show_feature_successfully_created_msg(self, feature_name, feature_id): message = QCoreApplication.translate( "WizardTranslations", "The new {} (t_id={}) was successfully created ")\ .format(feature_name, feature_id) self.__logger.info_msg(__name__, message) def show_feature_not_found_in_layer_msg(self): message = QCoreApplication.translate( "WizardTranslations", "'{}' tool has been closed. Feature not found in layer {}... It's not possible create it.") \ .format(self.__WIZARD_TOOL_NAME, self.__editing_layer_name) self.__logger.info_msg(__name__, message) def show_feature_not_found_in_layer_warning(self): self.__logger.warning( __name__, "Feature not found in layer {} ...".format( self.__editing_layer_name)) def show_select_a_source_layer_warning(self): message = QCoreApplication.translate( "WizardTranslations", "Select a source layer to set the field mapping to '{}'.") \ .format(self.__editing_layer_name) self.__logger.warning_msg(__name__, message)
class ConfigDBsSupported(QObject): def __init__(self): self.id_default_db = None self._db_factories = dict() self.logger = Logger() self._init_db_factories() def _init_db_factories(self): # PostgreSQL/PostGIS db_factory = PGFactory() self._db_factories[db_factory.get_id()] = db_factory self.id_default_db = db_factory.get_id( ) # Make PostgreSQL the default DB engine # GeoPackage db_factory = GPKGFactory() self._db_factories[db_factory.get_id()] = db_factory # MS SQL Server pyodbc_installed = is_pyodbc_available() libqt5sql5_odbc_installed = is_libqt5sql5_odbc_available() driver_odbc_available = check_if_odbc_exists() if pyodbc_installed and libqt5sql5_odbc_installed and driver_odbc_available: from asistente_ladm_col.lib.db.mssql_factory import MSSQLFactory db_factory = MSSQLFactory() self._db_factories[db_factory.get_id()] = db_factory if not pyodbc_installed: self.logger.warning( __name__, QCoreApplication.translate( "ConfigDBsSupported", "MS SQL Server could not be configured. Library 'pyodbc' is missing!" )) if not libqt5sql5_odbc_installed: self.logger.warning( __name__, QCoreApplication.translate( "ConfigDBsSupported", "MS SQL Server could not be configured. Library 'libqt5sql5-odbc' is missing!" )) if not driver_odbc_available: self.logger.warning( __name__, QCoreApplication.translate( "ConfigDBsSupported", "MS SQL Server could not be configured. There is not any odbc driver installed!" )) def get_db_factories(self): return self._db_factories def get_db_factory(self, engine): return self._db_factories[ engine] if engine in self._db_factories else None
class RoleRegistry(QObject, metaclass=SingletonQObject): """ Manage all role information. Current role can also be got/set from this class. Roles can set their own GUI configuration, their own LADM-COL supported models, their own quality rules, etc. """ active_role_changed = pyqtSignal(str) # New active role key COMMON_ACTIONS = [ # Common actions for all roles ACTION_LOAD_LAYERS, ACTION_SCHEMA_IMPORT, ACTION_IMPORT_DATA, ACTION_EXPORT_DATA, ACTION_SETTINGS, ACTION_HELP, ACTION_ABOUT ] def __init__(self): QObject.__init__(self) self.logger = Logger() self.app = AppInterface() self._registered_roles = dict() self._default_role = BASIC_ROLE def register_role(self, role_key, role_dict): """ Register roles for the LADM-COL assistant. Roles have access only to certain GUI controls, to certain LADM-COL models and to certain quality rules. :param role_key: Role unique identifier :param role_dict: Dictionary with the following information: ROLE_NAME: Name of the role ROLE_DESCRIPTION: Explains what this role is about ROLE_ACTIONS: List of actions a role has access to ROLE_MODELS: List of models and their configuration for the current role :return: Whether the role was successfully registered or not. """ valid = False if ROLE_NAME in role_dict and ROLE_DESCRIPTION in role_dict and ROLE_ACTIONS in role_dict and \ ROLE_GUI_CONFIG in role_dict and ROLE_MODELS in role_dict: self._registered_roles[role_key] = deepcopy(role_dict) valid = True else: self.logger.error(__name__, "Role '{}' is not defined correctly and could not be registered! Check the role_dict parameter.".format(role_key)) return valid def get_active_role(self): return self.app.settings.active_role or self._default_role def get_active_role_name(self): return self.get_role_name(self.get_active_role()) def active_role_already_set(self): """ Whether we have set an active role already or not. :return: True if the current_role_key variable is stored in QSettings. False otherwise. """ return self.app.settings.active_role is not None def set_active_role(self, role_key, emit_signal=True): """ Set the active role for the plugin. :param role_key: Key to identify the role. :param emit_signal: Whether the active_role_changed should be emitted or not. A False argument should be passed if the plugin config refresh will be called manually, for instance, because it is safer to call a GUI refresh after closing some plugin dialogs. :return: Whether the role was successfully changed or not in the role registry. """ res = False if role_key in self._registered_roles: res = True else: self.logger.warning(__name__, "Role '{}' was not found, the default role is now active.".format(role_key)) role_key = self._default_role self.app.settings.active_role = role_key self.logger.info(__name__, "Role '{}' is now active!".format(role_key)) if emit_signal: self.active_role_changed.emit(role_key) return res def set_active_default_role(self, emit_signal=True): return self.set_active_role(self._default_role, emit_signal) def get_roles_info(self): return {k: v[ROLE_NAME] for k,v in self._registered_roles.items()} def get_role_name(self, role_key): if role_key not in self._registered_roles: self.logger.error(__name__, "Role '{}' was not found, returning default role's name".format(role_key)) role_key = self._default_role return self._registered_roles[role_key][ROLE_NAME] def get_role_description(self, role_key): if role_key not in self._registered_roles: self.logger.error(__name__, "Role '{}' was not found, returning default role's decription".format(role_key)) role_key = self._default_role return self._registered_roles[role_key][ROLE_DESCRIPTION] def get_role_actions(self, role_key): if role_key not in self._registered_roles: self.logger.error(__name__, "Role '{}' was not found, returning default role's actions.".format(role_key)) role_key = self._default_role return list(set(self._registered_roles[role_key][ROLE_ACTIONS] + self.COMMON_ACTIONS)) def get_role_gui_config(self, role_key): if role_key not in self._registered_roles: self.logger.error(__name__, "Role '{}' was not found, returning default role's GUI configuration.".format(role_key)) role_key = self._default_role return self._registered_roles[role_key][ROLE_GUI_CONFIG] def get_role_models(self, role_key): """ Normally you wouldn't need this but LADMColModelRegistry, which is anyway updated when the role changes """ if role_key not in self._registered_roles: self.logger.error(__name__, "Role '{}' was not found, returning default role's models.".format(role_key)) role_key = self._default_role return self._registered_roles[role_key][ROLE_MODELS] def get_active_role_supported_models(self): role_key = self.get_active_role() role_models = self.get_role_models(role_key) return role_models[ROLE_SUPPORTED_MODELS] def get_role_quality_rules(self, role_key): if role_key not in self._registered_roles: self.logger.error(__name__, "Role '{}' was not found, returning default role's quality rules.".format(role_key)) role_key = self._default_role return self._registered_roles[role_key][ROLE_QUALITY_RULES] def get_role_db_source(self, role_key): if role_key not in self._registered_roles: self.logger.error(__name__, "Role '{}' was not found, returning default role's db source.".format(role_key)) role_key = self._default_role return self._registered_roles[role_key][ROLE_DB_SOURCE] if ROLE_DB_SOURCE in self._registered_roles[role_key] else None
class XTFModelConverterRegistry(metaclass=Singleton): """ Registry of supported model converters. """ def __init__(self): self.logger = Logger() self.app = AppInterface() self.__converters = dict() # {converter_key1: LADMColModelConverter1, ...} # Register default models self.register_model_converter(Survey10To11Converter()) self.register_model_converter(Survey11To10Converter()) def register_model_converter(self, converter): """ :param converter: LADMColModelConverter instance. :return: True if the converter was registered, False otherwise. """ if not isinstance(converter, AbstractLADMColModelConverter): self.logger.warning(__name__, "The converter '{}' is not a 'LADMColModelConverter' instance!".format(converter.id())) return False if not converter.is_valid(): self.logger.warning(__name__, "The converter '{}' is not valid! Check the converter definition!".format(converter.id())) return False if converter.id() in self.__converters: self.logger.warning(__name__, "The converter '{}' is already registered.".format(converter.id())) return False self.__converters[converter.id()] = converter self.logger.info(__name__, "Model converter '{}' has been registered!".format(converter.id())) return True def unregister_model_converter(self, converter_key): """ Unregisters a model converter. :param converter_key: Id of the converter to unregister. :return: True if the converter was unregistered, False otherwise. """ if converter_key not in self.__converters: self.logger.error(__name__, "Converter '{}' was not found in registered model converters, therefore, it cannot be unregistered!".format(converter_key)) return False self.__converters[converter_key] = None del self.__converters[converter_key] self.logger.info(__name__, "Model converter '{}' has been unregistered!".format(converter_key)) return True def get_converter(self, converter_key): converter = self.__converters.get(converter_key, None) # To avoid exceptions if not converter: self.logger.critical(__name__, "No model converter found with key '{}'".format(converter_key)) return converter def get_converters_for_models(self, models): converters = dict() # {converter_key_1: converter_display_name_1, ...] for converter_key, converter in self.__converters.items(): for model in models: if converter.supports_source_model(model): converters[converter_key] = converter.display_name() return converters
class QualityRuleRegistry(metaclass=Singleton): """ Registry of supported quality rules. """ def __init__(self): self.logger = Logger() self.app = AppInterface() self.__quality_rules = dict() # {quality_rule_key1: QualityRule1, ...} # Register default quality rules self.register_quality_rule(QRValidateDataAgainstModel()) # QR_ILIVALIDATORR0001 self.register_quality_rule(QROverlappingBoundaryPoints()) # QR_IGACR1001 self.register_quality_rule(QRBoundaryPointsNotCoveredByBoundaryNodes()) # QR_IGACR1003 self.register_quality_rule(QROverlappingBoundaries()) # QR_IGACR2001 self.register_quality_rule(QROverlappingBuildings()) # QR_IGACR3002 self.register_quality_rule(QRGapsInPlots()) # QR_IGACR3006 self.register_quality_rule(QRMultiPartsInRightOfWay()) # QR_IGACR3007 self.register_quality_rule(QRParcelRightRelationship()) # QR_IGACR4001 self.register_quality_rule(QRParcelWithInvalidDepartmentCode()) # QR_IGACR4003 self.register_quality_rule(QRParcelWithInvalidMunicipalityCode()) # QR_IGACR4004 self.register_quality_rule(QRParcelWithInvalidParcelNumber()) # QR_IGACR4005 self.register_quality_rule(QRParcelWithInvalidPreviousParcelNumber()) # QR_IGACR4006 self.register_quality_rule(QRValidateNaturalParty()) # QR_IGACR4007 self.register_quality_rule(QRValidateLegalParty()) # QR_IGACR4008 self.register_quality_rule(QRDuplicateBoundaryPointRecords()) # QR_IGACR4011 self.register_quality_rule(QRDuplicateSurveyPointRecords()) # QR_IGACR4012 self.register_quality_rule(QRDuplicateControlPointRecords()) # QR_IGACR4013 self.register_quality_rule(QRDuplicateBoundaryRecords()) # QR_IGACR4014 self.register_quality_rule(QRDuplicateBuildingRecords()) # QR_IGACR4016 self.register_quality_rule(QRDuplicateBuildingUnitRecords()) # QR_IGACR4017 self.register_quality_rule(QRDuplicatePartyRecords()) # QR_IGACR4019 self.register_quality_rule(QRDuplicateRightRecords()) # QR_IGACR4020 self.register_quality_rule(QRDuplicateRestrictionRecords()) # QR_IGACR4021 self.register_quality_rule(QRDuplicateAdministrativeSourceRecords()) # QR_IGACR4022 def register_quality_rule(self, quality_rule): """ Registers a quality rule. :param quality_rule: QualityRule instance. :return: True if the quality rule was registered, False otherwise. """ if not isinstance(quality_rule, AbstractQualityRule): self.logger.warning(__name__, "The quality rule '{}' is not an 'AbstractQualityRule' instance!".format(quality_rule.id())) return False if not quality_rule.is_valid(): self.logger.warning(__name__, "The quality rule '{}' is not valid! Check the quality rule definition!".format( quality_rule.id())) return False if quality_rule.id() in self.__quality_rules: self.logger.warning(__name__, "The quality rule '{}' is already registered.".format(quality_rule.id())) return False self.__quality_rules[quality_rule.id()] = quality_rule self.logger.info(__name__, "Quality rule '{}' has been registered!".format(quality_rule.id())) return True def unregister_quality_rule(self, quality_rule_id): """ Unregisters a quality rule by id. :param quality_rule_id: Id of the quality rule to unregister. :return: True if the quality rule was unregistered, False otherwise. """ if quality_rule_id not in self.__quality_rules: self.logger.error(__name__, "Quality rule '{}' was not found in registered quality rules, therefore, it cannot be unregistered!".format(quality_rule_id)) return False self.__quality_rules[quality_rule_id] = None del self.__quality_rules[quality_rule_id] self.logger.info(__name__, "Quality rule '{}' has been unregistered!".format(quality_rule_id)) return True def get_quality_rule(self, quality_rule_id): qr = self.__quality_rules.get(quality_rule_id, None) if not qr: self.logger.warning(__name__, "Quality rule '{}' is not registered, therefore it cannot be obtained!".format(quality_rule_id)) return qr def get_qrs_per_role_and_models(self, db, as_dict=True): """ :param as_dict: Boolean. If False, the result is returned as a list or rule keys """ qrs = dict() role_registry = RoleRegistry() role_qrs = role_registry.get_role_quality_rules(role_registry.get_active_role()) if role_qrs == ALL_QUALITY_RULES: role_qrs = self.__quality_rules if role_qrs: db_models = db.get_models() model_registry = LADMColModelRegistry() for qr in role_qrs: # First check if the role QR is registered if qr in self.__quality_rules: # Then check if the models required by the QR are in the DB req_models = self.__quality_rules[qr].models() num_models = len(req_models) all_models_found = True if num_models: # We don't check models if a QR has no required models (e.g., iliValidator) for req_model in req_models: model = model_registry.model(req_model) model_key = model.full_name() if model_key and model_key not in db_models: all_models_found = False self.logger.debug(__name__, "Model '{}' not found in the DB. QR '{}' cannot be listed.".format( model_key, qr )) break if all_models_found: qrs[qr] = self.__quality_rules[qr] return qrs if as_dict else list(qrs.keys())
class CreateGroupPartyOperation(QDialog, DIALOG_UI): WIZARD_NAME = "CreateGroupPartyOperationWizard" WIZARD_TOOL_NAME = QCoreApplication.translate(WIZARD_NAME, "Create group party") def __init__(self, iface, db, qgis_utils, parent=None): QDialog.__init__(self) self.setupUi(self) self.iface = iface self._db = db self.qgis_utils = qgis_utils self.logger = Logger() self.names = self._db.names self.help_strings = HelpStrings() self.data = {} # {t_id: [display_text, denominator, numerator]} self.current_selected_parties = [] # [t_ids] self.parties_to_group = {} # {t_id: [denominator, numerator]} self._layers = { self.names.OP_GROUP_PARTY_T: { 'name': self.names.OP_GROUP_PARTY_T, 'geometry': None, LAYER: None }, self.names.OP_PARTY_T: { 'name': self.names.OP_PARTY_T, 'geometry': None, LAYER: None }, self.names.MEMBERS_T: { 'name': self.names.MEMBERS_T, 'geometry': None, LAYER: None }, self.names.FRACTION_S: { 'name': self.names.FRACTION_S, 'geometry': None, LAYER: None }, self.names.COL_GROUP_PARTY_TYPE_D: { 'name': self.names.COL_GROUP_PARTY_TYPE_D, 'geometry': None, LAYER: None } } # Fill combo of types col_group_party_type_table = self.qgis_utils.get_layer( self._db, self.names.COL_GROUP_PARTY_TYPE_D, None, True) if not col_group_party_type_table: return for feature in col_group_party_type_table.getFeatures(): self.cbo_group_type.addItem(feature[self.names.DISPLAY_NAME_F], feature[self.names.T_ID_F]) self.txt_search_party.setText("") self.btn_select.setEnabled(False) self.btn_deselect.setEnabled(False) self.tbl_selected_parties.setColumnCount(3) self.tbl_selected_parties.setColumnWidth(0, 140) self.tbl_selected_parties.setColumnWidth(1, 90) self.tbl_selected_parties.setColumnWidth(2, 90) self.tbl_selected_parties.sortItems(0, Qt.AscendingOrder) self.txt_search_party.textEdited.connect(self.search) self.lst_all_parties.itemSelectionChanged.connect( self.selection_changed_all) self.tbl_selected_parties.itemSelectionChanged.connect( self.selection_changed_selected) self.tbl_selected_parties.cellChanged.connect(self.valueEdited) self.btn_select_all.clicked.connect(self.select_all) self.btn_deselect_all.clicked.connect(self.deselect_all) self.btn_select.clicked.connect(self.select) self.btn_deselect.clicked.connect(self.deselect) self.buttonBox.helpRequested.connect(self.show_help) self.bar = QgsMessageBar() self.bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.layout().addWidget(self.bar, 0, 0, Qt.AlignTop) self.rejected.connect(self.close_wizard) def closeEvent(self, e): # It's necessary to prevent message bar alert pass def required_layers_are_available(self): layers_are_available = self.qgis_utils.required_layers_are_available( self._db, self._layers, self.WIZARD_TOOL_NAME) return layers_are_available def load_parties_data(self): expression = QgsExpression( LayerConfig.get_dict_display_expressions( self.names)[self.names.OP_PARTY_T]) context = QgsExpressionContext() data = dict() for feature in self._layers[ self.names.OP_PARTY_T][LAYER].getFeatures(): context.setFeature(feature) expression.prepare(context) value = expression.evaluate(context) data[feature[self.names.T_ID_F]] = [ value if value != NULL else None, 0, 0 ] self.set_parties_data(data) def set_parties_data(self, parties_data): """ Initialize parties data. :param parties_data: Dictionary {t_id: [display_text, denominator, numerator]} :type parties_data: dict """ self.data = parties_data self.update_lists() def search(self, text): self.update_lists(True) def selection_changed_all(self): self.btn_select.setEnabled(len(self.lst_all_parties.selectedItems())) def selection_changed_selected(self): self.btn_deselect.setEnabled( len(self.tbl_selected_parties.selectedItems())) def select_all(self): """ SLOT. Select all parties listed from left list widget. """ items_ids = [] for index in range(self.lst_all_parties.count()): items_ids.append( self.lst_all_parties.item(index).data(Qt.UserRole)) self.add_parties_to_selected(items_ids) def deselect_all(self): """ SLOT. Remove all parties from left list widget. """ items_ids = [] for index in range(self.tbl_selected_parties.rowCount()): items_ids.append( self.tbl_selected_parties.item(index, 0).data(Qt.UserRole)) self.remove_parties_from_selected(items_ids) def select(self): """ SLOT. Select all parties highlighted in left list widget. """ self.add_parties_to_selected([ item.data(Qt.UserRole) for item in self.lst_all_parties.selectedItems() ]) def deselect(self): """ SLOT. Remove all parties highlighted in right list widget. """ self.remove_parties_from_selected([ item.data(Qt.UserRole) for item in self.tbl_selected_parties.selectedItems() if item.column() == 0 ]) def add_parties_to_selected(self, parties_ids): self.current_selected_parties.extend(parties_ids) self.update_lists() def remove_parties_from_selected(self, parties_ids): for party_id in parties_ids: self.current_selected_parties.remove(party_id) if party_id in self.parties_to_group: del self.parties_to_group[party_id] self.update_lists() def update_lists(self, only_update_all_list=False): """ Update left list widget and optionally the right one. :param only_update_all_list: Only update left list widget. :type only_update_all_list: bool """ # All parties self.lst_all_parties.clear() if self.txt_search_party.text(): tmp_parties = { i: d for i, d in self.data.items() if self.txt_search_party.text().lower() in d[0].lower() } else: tmp_parties = copy.deepcopy(self.data) # Copy all! for party_id in self.current_selected_parties: if party_id in tmp_parties: del tmp_parties[party_id] for i, d in tmp_parties.items(): item = QListWidgetItem(d[0]) item.setData(Qt.UserRole, i) self.lst_all_parties.addItem(item) if not only_update_all_list: # Selected parties self.tbl_selected_parties.clearContents() self.tbl_selected_parties.setRowCount( len(self.current_selected_parties)) self.tbl_selected_parties.setColumnCount(3) self.tbl_selected_parties.setSortingEnabled(False) for row, party_id in enumerate(self.current_selected_parties): item = QTableWidgetItem(self.data[party_id][0]) item.setFlags(item.flags() & ~Qt.ItemIsEditable) item.setData(Qt.UserRole, party_id) self.tbl_selected_parties.setItem(row, 0, item) value_denominator = self.parties_to_group[party_id][ 0] if party_id in self.parties_to_group else self.data[ party_id][1] self.tbl_selected_parties.setItem( row, 1, QTableWidgetItem(str(value_denominator))) value_numerator = self.parties_to_group[party_id][ 1] if party_id in self.parties_to_group else self.data[ party_id][2] self.tbl_selected_parties.setItem( row, 2, QTableWidgetItem(str(value_numerator))) self.tbl_selected_parties.setSortingEnabled(True) def valueEdited(self, row, column): """ SLOT. Update either the denominator or the numerator for given row. :param row: Edited row :type row: int :param column: Edited column :type column: int """ if column != 0: party_id = self.tbl_selected_parties.item(row, 0).data(Qt.UserRole) value_denominator = self.tbl_selected_parties.item(row, 1).text() # While creating a row and the second column is created, the third # one doesn't exist, so use the value already stored for that case value_numerator = self.parties_to_group[party_id][ 1] if party_id in self.parties_to_group else 0 if self.tbl_selected_parties.item(row, 2) is not None: value_numerator = self.tbl_selected_parties.item(row, 2).text() self.parties_to_group[party_id] = [ value_denominator, value_numerator ] def accept(self): """ Overwrite the dialog's `accept <https://doc.qt.io/qt-5/qdialog.html#accept>`_ SLOT to store selected parties and numerator-denominator before closing the dialog. """ self.parties_to_group = {} for index in range(self.tbl_selected_parties.rowCount()): k = self.tbl_selected_parties.item(index, 0).data(Qt.UserRole) try: v_n = int(self.tbl_selected_parties.item(index, 1).text()) except ValueError as e: self.show_message( QCoreApplication.translate( "WizardTranslations", "There are some invalid values in the numerator column. Fix them before continuing..." ), Qgis.Warning) return try: v_d = int(self.tbl_selected_parties.item(index, 2).text()) except ValueError as e: self.show_message( QCoreApplication.translate( "WizardTranslations", "There are some invalid values in the denominator column. Fix them before continuing..." ), Qgis.Warning) return self.parties_to_group[k] = [v_n, v_d] name = self.txt_group_name.text() group_party_type = self.cbo_group_type.itemData( self.cbo_group_type.currentIndex()) dict_params = { self.names.COL_PARTY_T_NAME_F: name, self.names.COL_GROUP_PARTY_T_TYPE_F: group_party_type, 'porcentajes': self.parties_to_group } res, msg = self.validate_group_party(dict_params) if not res: self.show_message(msg, Qgis.Warning) return self.save_group_party(self._db, [dict_params]) def validate_group_party(self, params): name = params[self.names.COL_PARTY_T_NAME_F] group_party_type = params[self.names.COL_GROUP_PARTY_T_TYPE_F] porcentajes = params['porcentajes'] if not porcentajes: return (False, QCoreApplication.translate( "CreateGroupParty", "You need to select some parties to create a group.")) elif len(porcentajes) == 1: return ( False, QCoreApplication.translate( "CreateGroupParty", "There is just one party, you need to add at least two parties to a group." )) there_percents = False fraction = Fraction() for t, nd in porcentajes.items(): if porcentajes[t] != [0, 0]: there_percents = True break if there_percents: for t, nd in porcentajes.items(): if porcentajes[t][1] == 0: return ( False, QCoreApplication.translate( "CreateGroupParty", "There are denominators equal to zero. You need to change those values." )) elif porcentajes[t][1] < porcentajes[t][0]: return ( False, QCoreApplication.translate( "CreateGroupParty", "The denominator cannot be less than the numerator." )) else: fraction = Fraction(porcentajes[t][0], porcentajes[t][1]) + fraction if fraction != 1.0: return (False, QCoreApplication.translate( "CreateGroupParty", "The sum of the fractions must be equal to one.")) return (True, QCoreApplication.translate("CreateGroupParty", "Validation passed!")) def show_message(self, message, level): self.bar.clearWidgets( ) # Remove previous messages before showing a new one self.bar.pushMessage(message, level, 10) def save_group_party(self, db, params): """ Save group party data into associated tables: self.names.OP_GROUP_PARTY_T, self.names.MEMBERS_T and self.names.FRACTION_S. params: List of dicts, where each dict is an independent group party: { self.names.COL_PARTY_T_NAME_F: '', self.names.COL_GROUP_PARTY_T_TYPE_F: '', 'porcentajes': { 't_id_miembro': [20, 100], # numerador/denominador 't_id_miembro2': [40, 100] } } """ # Disconnect from previous runs self.disconnect_signals() for group in params: # Create connections to react when a group party is stored to the DB self._layers[self.names.OP_GROUP_PARTY_T][ LAYER].committedFeaturesAdded.connect( partial(self.finish_group_party_saving, group['porcentajes'])) # First save the group party new_feature = QgsVectorLayerUtils().createFeature( self._layers[self.names.OP_GROUP_PARTY_T][LAYER]) new_feature.setAttribute( self.names.COL_GROUP_PARTY_T_TYPE_F, group[self.names.COL_GROUP_PARTY_T_TYPE_F]) new_feature.setAttribute(self.names.COL_PARTY_T_NAME_F, group[self.names.COL_PARTY_T_NAME_F]) # TODO: Remove when local id and working space are defined new_feature.setAttribute(self.names.OID_T_LOCAL_ID_F, 1) new_feature.setAttribute(self.names.OID_T_NAMESPACE_F, self.names.OP_GROUP_PARTY_T) # TODO: Gui should allow users to ented namespace, local_id and date values #new_feature.setAttribute("p_espacio_de_nombres", self.names.OP_GROUP_PARTY_T) #new_feature.setAttribute("p_local_id", '0') #new_feature.setAttribute("comienzo_vida_util_version", 'now()') self.logger.info(__name__, "Saving Group Party: {}".format(group)) with edit(self._layers[self.names.OP_GROUP_PARTY_T][LAYER]): self._layers[self.names.OP_GROUP_PARTY_T][LAYER].addFeature( new_feature) def finish_group_party_saving(self, members, layer_id, features): try: self._layers[self.names.OP_GROUP_PARTY_T][ LAYER].committedFeaturesAdded.disconnect() except TypeError as e: pass message = QCoreApplication.translate( "WizardTranslations", "'{}' tool has been closed because an error occurred while trying to save the data." ).format(self.WIZARD_TOOL_NAME) if len(features) != 1: message = QCoreApplication.translate( "WizardTranslations", "'{}' tool has been closed. We should have got only one group party... We cannot do anything with {} group parties" ).format(self.WIZARD_TOOL_NAME, len(features)) self.logger.warning( __name__, "We should have got only one group party... We cannot do anything with {} group parties" .format(len(features))) else: fid = features[0].id() if not self._layers[self.names.OP_GROUP_PARTY_T][LAYER].getFeature( fid).isValid(): self.logger.warning( __name__, "Feature not found in table Group Party...") else: group_party_id = self._layers[self.names.OP_GROUP_PARTY_T][ LAYER].getFeature(fid)[self.names.T_ID_F] # Now save members party_ids = list() for party_id, fraction in members.items(): # Create connections to react when a group party is stored to the DB self._layers[self.names.MEMBERS_T][ LAYER].committedFeaturesAdded.connect( partial(self.finish_member_saving, fraction)) new_feature = QgsVectorLayerUtils().createFeature( self._layers[self.names.MEMBERS_T][LAYER]) new_feature.setAttribute( self.names.MEMBERS_T_GROUP_PARTY_F, group_party_id) new_feature.setAttribute(self.names.MEMBERS_T_PARTY_F, party_id) self.logger.info( __name__, "Saving group party's member ({}: {}).".format( group_party_id, party_id)) with edit(self._layers[self.names.MEMBERS_T][LAYER]): self._layers[self.names.MEMBERS_T][LAYER].addFeature( new_feature) party_ids.append(party_id) if len(party_ids): message = QCoreApplication.translate( "WizardTranslations", "The new group party (t_id={}) was successfully created and associated with its corresponding party(ies) (t_id={})!" ).format(group_party_id, ", ".join([str(b) for b in party_ids])) else: message = QCoreApplication.translate( "WizardTranslations", "The new group party (t_id={}) was successfully created but this one wasn't associated with a party(ies)" ).format(group_party_id) self.close_wizard(message) def finish_member_saving(self, fraction, layer_id, features): try: self._layers[self.names. MEMBERS_T][LAYER].committedFeaturesAdded.disconnect() except TypeError as e: pass if len(features) != 1: self.logger.warning( __name__, "We should have got only one member... We cannot do anything with {} members" .format(len(features))) else: fid = features[0].id() if not self._layers[self.names.MEMBERS_T][LAYER].getFeature( fid).isValid(): self.logger.warning(__name__, "Feature not found in table Members...") else: member_id = self._layers[self.names.MEMBERS_T][ LAYER].getFeature(fid)[self.names.T_ID_F] if fraction == [0, 0]: return # And finally save fractions new_feature = QgsVectorLayerUtils().createFeature( self._layers[self.names.FRACTION_S][LAYER]) new_feature.setAttribute(self.names.FRACTION_S_MEMBER_F, member_id) new_feature.setAttribute(self.names.FRACTION_S_NUMERATOR_F, fraction[0]) new_feature.setAttribute(self.names.FRACTION_S_DENOMINATOR_F, fraction[1]) with edit(self._layers[self.names.FRACTION_S][LAYER]): self.logger.info( __name__, "Saving member's fraction ({}: {}).".format( member_id, fraction)) self._layers[self.names.FRACTION_S][LAYER].addFeature( new_feature) def close_wizard(self, message=None, show_message=True): if message is None: message = QCoreApplication.translate( "WizardTranslations", "'{}' tool has been closed.").format(self.WIZARD_TOOL_NAME) if show_message: self.logger.info_msg(__name__, message) self.disconnect_signals() self.close() def disconnect_signals(self): try: self._layers[self.names.OP_GROUP_PARTY_T][ LAYER].committedFeaturesAdded.disconnect() except TypeError as e: pass try: self._layers[self.names. MEMBERS_T][LAYER].committedFeaturesAdded.disconnect() except TypeError as e: pass def show_help(self): self.qgis_utils.show_help("group_party")
class STTaskManager(QObject): """ Retrieve tasks for a user from the Transitional System's Task Service and store them during the session. """ task_started = pyqtSignal(int) # task_id task_canceled = pyqtSignal(int) # task_id task_closed = pyqtSignal(int) # task_id def __init__(self): QObject.__init__(self) self.logger = Logger() self.__registered_tasks = dict() self.st_config = TransitionalSystemConfig() @_with_override_cursor def __retrieve_tasks(self, st_user, task_type=None, task_status=None): headers = { 'Authorization': "Bearer {}".format(st_user.get_token()), # 'User-Agent': "PostmanRuntime/7.20.1", 'Accept': "*/*", 'Cache-Control': "no-cache", # 'Postman-Token': "987c7fbf-af4d-42e8-adee-687f35f4a4a0,0547120a-6f8e-42a8-b97f-f052602cc7ff", # 'Host': "st.local:8090", 'Accept-Encoding': "gzip, deflate", 'Connection': "keep-alive", 'cache-control': "no-cache" } try: self.logger.debug(__name__, "Retrieving tasks from server...") response = requests.request("GET", self.st_config.ST_GET_TASKS_SERVICE_URL, headers=headers) except requests.ConnectionError as e: msg = self.st_config.ST_CONNECTION_ERROR_MSG.format(e) self.logger.warning(__name__, msg) return False, msg status_OK = response.status_code == 200 if status_OK: # Parse, create and register tasks response_data = json.loads(response.text) for task_data in response_data: task = STTask(task_data) if task.is_valid(): self.__register_task(task) else: if response.status_code == 500: self.logger.warning(__name__, self.st_config.ST_STATUS_500_MSG) elif response.status_code > 500 and response.status_code < 600: self.logger.warning(__name__, self.st_config.ST_STATUS_GT_500_MSG) elif response.status_code == 401: self.logger.warning(__name__, self.st_config.ST_STATUS_401_MSG) def get_tasks(self, st_user, task_type=None, task_status=None): """ Go to server for current tasks per user :param st_user: :param task_type: To filter task types. Still unused. :param task_status: To filter task statuses. Still unused. :return: dict of task ids with the corresponding task object """ # Each call refreshes the registered tasks. self.unregister_tasks() self.__retrieve_tasks(st_user, task_type, task_status) return self.__registered_tasks def get_task(self, task_id): task = self.__registered_tasks[task_id] if task_id in self.__registered_tasks else None if task is None: self.logger.warning(__name__, "Task {} not found!!!".format(task_id)) else: self.logger.info(__name__, "Task {} found!!!".format(task_id)) return task def __register_task(self, task): self.logger.debug(__name__, "Task {} registered!".format(task.get_id())) self.__registered_tasks[task.get_id()] = task def __unregister_task(self, task_id): self.logger.debug(__name__, "Task {} unregistered!".format(task_id)) self.__registered_tasks[task_id] = None del self.__registered_tasks[task_id] def unregister_tasks(self): for k,v in self.__registered_tasks.items(): self.__registered_tasks[k] = None self.__registered_tasks = dict() self.logger.info(__name__, "All tasks have been unregistered!") @_with_override_cursor def start_task(self, st_user, task_id): payload = {} headers = { 'Authorization': "Bearer {}".format(st_user.get_token()), } try: self.logger.debug(__name__, "Telling the server to start a task...") response = requests.request("PUT", self.st_config.ST_START_TASK_SERVICE_URL.format(task_id), headers=headers, data=payload) except requests.ConnectionError as e: msg = self.st_config.ST_CONNECTION_ERROR_MSG.format(e) self.logger.warning(__name__, msg) return False, msg status_OK = response.status_code == 200 if status_OK: # Parse response response_data = json.loads(response.text) self.logger.info(__name__, "Task id '{}' started in server!...".format(task_id)) self.logger.info_msg(__name__, QCoreApplication.translate("TaskManager", "The task '{}' was successfully started!".format( self.get_task(task_id).get_name()))) self.update_task_info(task_id, response_data) self.task_started.emit(task_id) else: if response.status_code == 500: self.logger.warning(__name__, self.st_config.ST_STATUS_500_MSG) elif response.status_code > 500 and response.status_code < 600: self.logger.warning(__name__, self.st_config.ST_STATUS_GT_500_MSG) elif response.status_code == 401: self.logger.warning(__name__, self.st_config.ST_STATUS_401_MSG) else: self.logger.warning(__name__, "Status code not handled: {}".format(response.status_code)) @_with_override_cursor def cancel_task(self, st_user, task_id, reason): payload = json.dumps({"reason": reason}) headers = { 'Authorization': "Bearer {}".format(st_user.get_token()), 'Content-Type': 'application/json' } try: self.logger.debug(__name__, "Telling the server to cancel a task...") response = requests.request("PUT", self.st_config.ST_CANCEL_TASK_SERVICE_URL.format(task_id), headers=headers, data=payload) except requests.ConnectionError as e: msg = self.st_config.ST_CONNECTION_ERROR_MSG.format(e) self.logger.warning(__name__, msg) return False, msg status_OK = response.status_code == 200 if status_OK: # No need to parse response this time, we'll ask tasks from server again anyways self.logger.info(__name__, "Task id '{}' canceled in server!".format(task_id)) self.logger.info_msg(__name__, QCoreApplication.translate("TaskManager", "The task '{}' was successfully canceled!".format(self.get_task(task_id).get_name()))) self.task_canceled.emit(task_id) else: if response.status_code == 500: self.logger.warning(__name__, self.st_config.ST_STATUS_500_MSG) elif response.status_code > 500 and response.status_code < 600: self.logger.warning(__name__, self.st_config.ST_STATUS_GT_500_MSG) elif response.status_code == 401: self.logger.warning(__name__, self.st_config.ST_STATUS_401_MSG) else: self.logger.warning(__name__, "Status code not handled: {}, payload: {}".format(response.status_code, payload)) @_with_override_cursor def close_task(self, st_user, task_id): payload = {} headers = { 'Authorization': "Bearer {}".format(st_user.get_token()), } try: self.logger.debug(__name__, "Telling the server to close a task...") response = requests.request("PUT", self.st_config.ST_CLOSE_TASK_SERVICE_URL.format(task_id), headers=headers, data=payload) except requests.ConnectionError as e: msg = self.st_config.ST_CONNECTION_ERROR_MSG.format(e) self.logger.warning(__name__, msg) return False, msg status_OK = response.status_code == 200 if status_OK: # No need to parse response this time, we'll ask tasks from server again anyways self.logger.success(__name__, "Task id '{}' closed in server!".format(task_id)) self.logger.success_msg(__name__, QCoreApplication.translate("TaskManager", "The task '{}' was successfully closed!".format( self.get_task(task_id).get_name()))) self.task_closed.emit(task_id) else: if response.status_code == 500: self.logger.warning(__name__, self.st_config.ST_STATUS_500_MSG) elif response.status_code > 500 and response.status_code < 600: self.logger.warning(__name__, self.st_config.ST_STATUS_GT_500_MSG) elif response.status_code == 401: self.logger.warning(__name__, self.st_config.ST_STATUS_401_MSG) elif response.status_code == 422: response_data = json.loads(response.text) msg = QCoreApplication.translate("STSession", QCoreApplication.translate("TaskManager", "Task not closed! Details: {}").format(response_data['message'] if 'message' in response_data else "Unreadable response from server.")) self.logger.warning_msg(__name__, msg) else: self.logger.warning(__name__, "Status code not handled: {}".format(response.status_code)) def update_task_info(self, task_id, task_data): task = STTask(task_data) if task.is_valid(): self.__unregister_task(task_id) self.__register_task(task)
class RightOfWay(QObject): def __init__(self): QObject.__init__(self) self.logger = Logger() self.app = AppInterface() self._right_of_way_line_layer = None self.addedFeatures = None def fill_right_of_way_relations(self, db): layers = { db.names.LC_ADMINISTRATIVE_SOURCE_T: None, db.names.LC_PARCEL_T: None, db.names.LC_PLOT_T: None, db.names.LC_RESTRICTION_T: None, db.names.LC_RESTRICTION_TYPE_D: None, db.names.LC_RIGHT_OF_WAY_T: None, db.names.COL_RRR_SOURCE_T: None, db.names.LC_SURVEY_POINT_T: None, db.names.COL_UE_BAUNIT_T: None } # Load layers self.app.core.get_layers(db, layers, load=True) if not layers: return None exp = "\"{}\" = '{}'".format( db.names.ILICODE_F, LADMNames.RESTRICTION_TYPE_D_RIGHT_OF_WAY_ILICODE_VALUE) restriction_right_of_way_t_id = [ feature for feature in layers[ db.names.LC_RESTRICTION_TYPE_D].getFeatures(exp) ][0][db.names.T_ID_F] if layers[db.names.LC_PLOT_T].selectedFeatureCount() == 0 or layers[ db.names.LC_RIGHT_OF_WAY_T].selectedFeatureCount( ) == 0 or layers[ db.names.LC_ADMINISTRATIVE_SOURCE_T].selectedFeatureCount( ) == 0: if self.app.core.get_ladm_layer_from_qgis( db, db.names.LC_PLOT_T, EnumLayerRegistryType.IN_LAYER_TREE) is None: self.logger.message_with_button_load_layer_emitted.emit( QCoreApplication.translate( "RightOfWay", "First load the layer {} into QGIS and select at least one plot!" ).format(db.names.LC_PLOT_T), QCoreApplication.translate("RightOfWay", "Load layer {} now").format( db.names.LC_PLOT_T), db.names.LC_PLOT_T, Qgis.Warning) else: self.logger.warning_msg( __name__, QCoreApplication.translate( "RightOfWay", "Select at least one benefited plot, one right of way and at least one administrative source to create relations!" )) return else: # 1) Fill parcel-right of way in col_uebaunit ue_baunit_features = layers[db.names.COL_UE_BAUNIT_T].getFeatures() # Get unique pairs id_right_of_way-id_parcel existing_pairs = [ (ue_baunit_feature[db.names.COL_UE_BAUNIT_T_PARCEL_F], ue_baunit_feature[db.names.COL_UE_BAUNIT_T_LC_RIGHT_OF_WAY_F]) for ue_baunit_feature in ue_baunit_features ] existing_pairs = set(existing_pairs) plot_ids = [ f[db.names.T_ID_F] for f in layers[db.names.LC_PLOT_T].selectedFeatures() ] right_of_way_id = layers[ db.names.LC_RIGHT_OF_WAY_T].selectedFeatures()[0].attribute( db.names.T_ID_F) id_pairs = list() for plot in plot_ids: exp = "\"{uebaunit}\" = {plot}".format( uebaunit=db.names.COL_UE_BAUNIT_T_LC_PLOT_F, plot=plot) parcels = layers[db.names.COL_UE_BAUNIT_T].getFeatures(exp) for parcel in parcels: id_pair = (parcel.attribute( db.names.COL_UE_BAUNIT_T_PARCEL_F), right_of_way_id) id_pairs.append(id_pair) if len(id_pairs) < len(plot_ids): # If any relationship plot-parcel is not found, we don't need to continue self.logger.warning_msg( __name__, QCoreApplication.translate( "RightOfWay", "One or more pairs id_plot-id_parcel weren't found, this is needed to create benefited and restriction relations." )) return if id_pairs: new_features = list() for id_pair in id_pairs: if not id_pair in existing_pairs: new_feature = QgsVectorLayerUtils().createFeature( layers[db.names.COL_UE_BAUNIT_T]) new_feature.setAttribute( db.names.COL_UE_BAUNIT_T_PARCEL_F, id_pair[0]) new_feature.setAttribute( db.names.COL_UE_BAUNIT_T_LC_RIGHT_OF_WAY_F, id_pair[1]) self.logger.info( __name__, "Saving relationship in col_uebaunit (RightOfWay-Parcel: {}-{})" .format(id_pair[1], id_pair[0])) new_features.append(new_feature) layers[db.names.COL_UE_BAUNIT_T].dataProvider().addFeatures( new_features) self.logger.info_msg( __name__, QCoreApplication.translate( "RightOfWay", "{} out of {} records were saved into {}! {} out of {} records already existed in the database." ).format(len(new_features), len(id_pairs), db.names.COL_UE_BAUNIT_T, len(id_pairs) - len(new_features), len(id_pairs))) # 2) Fill restriction for involved parcels spatial_join_layer = processing.run( "native:joinattributesbylocation", { 'INPUT': layers[db.names.LC_PLOT_T], 'JOIN': QgsProcessingFeatureSourceDefinition( layers[db.names.LC_RIGHT_OF_WAY_T].id(), True), 'PREDICATE': [4], # Overlaps 'JOIN_FIELDS': [db.names.T_ID_F], 'METHOD': 0, # 1:M 'DISCARD_NONMATCHING': True, 'PREFIX': '', 'OUTPUT': 'TEMPORARY_OUTPUT' })['OUTPUT'] restriction_features = layers[ db.names.LC_RESTRICTION_T].getFeatures() existing_restriction_pairs = [ (restriction_feature[db.names.COL_BAUNIT_RRR_T_UNIT_F], restriction_feature[db.names.COL_RRR_T_DESCRIPTION_F]) for restriction_feature in restriction_features ] existing_restriction_pairs = set(existing_restriction_pairs) id_pairs_restriction = list() plots = [feature for feature in spatial_join_layer.getFeatures()] for plot in plots: exp = "\"{uebaunit}\" = {plot}".format( uebaunit=db.names.COL_UE_BAUNIT_T_LC_PLOT_F, plot=plot.attribute(db.names.T_ID_F)) parcels = [ feature for feature in layers[ db.names.COL_UE_BAUNIT_T].getFeatures(exp) ] if not parcels: self.logger.warning( __name__, "The plot t_id {} overlapping with the right of way, has no related parcel!" .format(plot[db.names.T_ID_F])) for parcel in parcels: id_pair_restriction = (parcel.attribute( db.names.COL_UE_BAUNIT_T_PARCEL_F), QCoreApplication.translate( "RightOfWay", "Right of way")) id_pairs_restriction.append(id_pair_restriction) new_restriction_features = list() if id_pairs_restriction: for id_pair in id_pairs_restriction: if not id_pair in existing_restriction_pairs: new_feature = QgsVectorLayerUtils().createFeature( layers[db.names.LC_RESTRICTION_T]) new_feature.setAttribute( db.names.COL_BAUNIT_RRR_T_UNIT_F, id_pair[0]) new_feature.setAttribute( db.names.COL_RRR_T_DESCRIPTION_F, id_pair[1]) new_feature.setAttribute( db.names.LC_RESTRICTION_T_TYPE_F, restriction_right_of_way_t_id) self.logger.info( __name__, "Saving restriction (RightOfWay-Parcel: {}-{})". format(id_pair[1], id_pair[0])) new_restriction_features.append(new_feature) layers[db.names.LC_RESTRICTION_T].dataProvider().addFeatures( new_restriction_features) self.logger.info_msg( __name__, QCoreApplication.translate( "RightOfWay", "{} out of {} records were saved into {}! {} out of {} records already existed in the database." ).format( len(new_restriction_features), len(id_pairs_restriction), db.names.LC_RESTRICTION_T, len(id_pairs_restriction) - len(new_restriction_features), len(id_pairs_restriction))) # 3) Fill relationship restriction-administrative source in col_rrrfuente administrative_source_ids = [ f[db.names.T_ID_F] for f in layers[ db.names.LC_ADMINISTRATIVE_SOURCE_T].selectedFeatures() ] source_relation_features = layers[ db.names.COL_RRR_SOURCE_T].getFeatures() existing_source_pairs = [ (source_relation_feature[db.names.COL_RRR_SOURCE_T_SOURCE_F], source_relation_feature[ db.names.COL_RRR_SOURCE_T_LC_RESTRICTION_F]) for source_relation_feature in source_relation_features ] existing_source_pairs = set(existing_source_pairs) rrr_source_relation_pairs = list() for administrative_source_id in administrative_source_ids: for restriction_feature in new_restriction_features: rrr_source_relation_pair = (administrative_source_id, restriction_feature.attribute( db.names.T_ID_F)) rrr_source_relation_pairs.append(rrr_source_relation_pair) new_rrr_source_relation_features = list() if rrr_source_relation_pairs: for id_pair in rrr_source_relation_pairs: if id_pair not in existing_source_pairs: new_feature = QgsVectorLayerUtils().createFeature( layers[db.names.COL_RRR_SOURCE_T]) new_feature.setAttribute( db.names.COL_RRR_SOURCE_T_SOURCE_F, id_pair[0]) new_feature.setAttribute( db.names.COL_RRR_SOURCE_T_LC_RESTRICTION_F, id_pair[1]) self.logger.info( __name__, "Saving Restriction-Source: {}-{}".format( id_pair[1], id_pair[0])) new_rrr_source_relation_features.append(new_feature) layers[db.names.COL_RRR_SOURCE_T].dataProvider().addFeatures( new_rrr_source_relation_features) self.logger.info_msg( __name__, QCoreApplication.translate( "RightOfWay", "{} out of {} records were saved into {}! {} out of {} records already existed in the database." ).format( len(new_rrr_source_relation_features), len(rrr_source_relation_pairs), db.names.COL_RRR_SOURCE_T, len(rrr_source_relation_pairs) - len(new_rrr_source_relation_features), len(rrr_source_relation_pairs)))
class STSession(QObject, metaclass=SingletonQObject): TOKEN_KEY = "Asistente-LADM_COL/transitional_system/token" login_status_changed = pyqtSignal( bool ) # Status of the login: True if a user is logged in, False otherwise logout_finished = pyqtSignal() def __init__(self): QObject.__init__(self) self.logger = Logger() self.task_manager = STTaskManager() self.__logged_user = None def login(self, user, password): msg = "" st_config = TransitionalSystemConfig() payload = st_config.ST_LOGIN_SERVICE_PAYLOAD.format(user, password) headers = { 'Content-Type': "application/x-www-form-urlencoded", 'Authorization': st_config.ST_LOGIN_AUTHORIZATION_CLIENT, 'Accept': "*/*", 'Cache-Control': "no-cache", 'Accept-Encoding': "gzip, deflate", 'Connection': "keep-alive", 'cache-control': "no-cache" } s = requests.Session() s.mount(st_config.ST_LOGIN_SERVICE_URL, HTTPAdapter(max_retries=0)) try: response = s.request("POST", st_config.ST_LOGIN_SERVICE_URL, data=payload, headers=headers) except requests.ConnectionError as e: msg = QCoreApplication.translate( "STSession", "There was an error accessing the login service. Details: {}" ).format(e) self.logger.warning(__name__, msg) return False, msg status_OK = response.status_code == 200 self.logger.info( __name__, "Login response status code: {}".format(response.status_code)) if status_OK: msg = QCoreApplication.translate( "STSession", "User logged in successfully in the Transitional System!") logged_data = json.loads(response.text) self.__logged_user = STLoggedUser( "{} {}".format(logged_data['first_name'], logged_data['last_name']), logged_data['email'], logged_data['roles'][0]['name'], logged_data['access_token']) QSettings().setValue( self.TOKEN_KEY, logged_data['access_token']) # Register (login) the user self.login_status_changed.emit(True) self.logger.info(__name__, msg) else: if response.status_code == 400: msg = QCoreApplication.translate( "STSession", "Wrong user name or password, change credentials and try again." ) elif response.status_code == 500: msg = QCoreApplication.translate( "STSession", "There is an error in the login server!") elif response.status_code > 500 and response.status_code < 600: msg = self.st_config.ST_STATUS_GT_500_MSG self.logger.warning(__name__, self.st_config.ST_STATUS_GT_500_MSG) elif response.status_code == 401: msg = QCoreApplication.translate( "STSession", "Unauthorized client! The server won't allow requests from this client." ) self.logger.warning(__name__, msg) return status_OK, msg def logout(self): msg = "" logged_out = False if self.is_user_logged(): QSettings().setValue(self.TOKEN_KEY, "") # Unregister (logout) the user self.__logged_user = None logged_out = True self.login_status_changed.emit(False) self.logout_finished.emit() self.task_manager.unregister_tasks() msg = QCoreApplication.translate( "STSession", "User was logged out successfully!") else: msg = QCoreApplication.translate( "STSession", "There was not logged in user! Therefore, no logout.") self.logger.info(__name__, msg) return logged_out, msg def get_logged_st_user(self): return self.__logged_user def get_logged_role(self): return self.__logged_user.get_role( ) if self.__logged_user is not None else None def is_user_logged(self): return self.__logged_user is not None
class LADMQueryController(QObject): close_view_requested = pyqtSignal() zoom_to_features_requested = pyqtSignal(QgsVectorLayer, list, dict) # layer, ids, t_ids def __init__(self, db, ladm_data): QObject.__init__(self) self._db = db self._ladm_data = ladm_data self.logger = Logger() self.app = AppInterface() self.clipboard = QApplication.clipboard() self._layers = dict() self._ladm_queries = ConfigDBsSupported().get_db_factory( self._db.engine).get_ladm_queries() self._restart_dict_of_layers() self._add_layers() # To cache informal parcels, self._informal_parcels_info = tuple( ) # ([parcel_t_id: parcel_number], [,], [,], ...) self._informal_index = -1 self._informal_parcels_len = 0 # To avoid calculating this each time def _add_layers(self): self.app.core.get_layers(self._db, self._layers, load=True) if not self._layers: self._restart_dict_of_layers() # Let it ready for the next call return None # Layer was found, listen to its removal so that we can deactivate the custom tool when that occurs self.disconnect_plot_layer() self._layers[self._db.names.LC_PLOT_T].willBeDeleted.connect( self._plot_layer_removed) # Layer was found, listen to its removal so that we can update the variable properly self.disconnect_parcel_layer() self._layers[self._db.names.LC_PARCEL_T].willBeDeleted.connect( self._parcel_layer_removed) # Layer was found, listen to its removal so that we can update the variable properly try: self._layers[ self._db.names.COL_UE_BAUNIT_T].willBeDeleted.disconnect( self._uebaunit_table_removed) except: pass self._layers[self._db.names.COL_UE_BAUNIT_T].willBeDeleted.connect( self._uebaunit_table_removed) def _restart_dict_of_layers(self): self._layers = { self._db.names.LC_PLOT_T: None, self._db.names.LC_PARCEL_T: None, self._db.names.COL_UE_BAUNIT_T: None } def plot_layer(self): if self._layers[self._db.names.LC_PLOT_T] is None: self._add_layers() return self._layers[self._db.names.LC_PLOT_T] def parcel_layer(self): if self._layers[self._db.names.LC_PARCEL_T] is None: self._add_layers() return self._layers[self._db.names.LC_PARCEL_T] def uebaunit_table(self): if self._layers[self._db.names.COL_UE_BAUNIT_T] is None: self._add_layers() return self._layers[self._db.names.COL_UE_BAUNIT_T] def _plot_layer_removed(self): # The required layer was removed self.close_view_requested.emit() self._layers[self._db.names.LC_PLOT_T] = None def _parcel_layer_removed(self): self._layers[self._db.names.LC_PARCEL_T] = None def _uebaunit_table_removed(self): self._layers[self._db.names.COL_UE_BAUNIT_T] = None def disconnect_plot_layer(self): try: self._layers[self._db.names.LC_PLOT_T].willBeDeleted.disconnect( self._plot_layer_removed) except: pass def disconnect_parcel_layer(self): try: self._layers[self._db.names.LC_PARCEL_T].willBeDeleted.disconnect( self._parcel_layer_removed) except: pass def parcel_layer_name(self): return self._db.names.LC_PARCEL_T def t_id_name(self): return self._db.names.T_ID_F def parcel_number_name(self): return self._db.names.LC_PARCEL_T_PARCEL_NUMBER_F def previous_parcel_number_name(self): return self._db.names.LC_PARCEL_T_PREVIOUS_PARCEL_NUMBER_F def fmi_name(self): return self._db.names.LC_PARCEL_T_FMI_F def create_model(self, records): return TreeModel(self._db.names, data=records) def update_db_connection(self, db, ladm_col_db, db_source): self.close_view_requested.emit() def copy_value(self, value): self.clipboard.setText(str(value)) def open_url(self, url): webbrowser.open(url) def zoom_to_feature(self, layer, t_id): self.zoom_to_features_requested.emit(layer, list(), {self._db.names.T_ID_F: [t_id]}) def zoom_to_plots(self, plot_ids): self.zoom_to_features_requested.emit(self.plot_layer(), plot_ids, dict()) def zoom_to_resulting_plots(self, records): # Zoom to plots retrieved from a search plot_t_ids = self._get_plot_t_ids_from_basic_info(records) if plot_t_ids: features = self._ladm_data.get_features_from_t_ids( self._layers[self._db.names.LC_PLOT_T], self._db.names.T_ID_F, plot_t_ids, True, True) plot_ids = [feature.id() for feature in features] self.zoom_to_features_requested.emit(self.plot_layer(), plot_ids, dict()) self.plot_layer().selectByIds(plot_ids) def _get_plot_t_ids_from_basic_info(self, records): res = [] if records: if self._db.names.LC_PLOT_T in records: for element in records[self._db.names.LC_PLOT_T]: res.append(element['id']) return res def open_feature_form(self, layer, t_id): # Note that it is important to fetch all feature attributes from the next call features = self._ladm_data.get_features_from_t_ids( layer, self._db.names.T_ID_F, [t_id], no_geometry=True) if features: self.app.gui.open_feature_form(layer, features[0]) else: self.logger.warning( __name__, "No feature found in layer '{}' with t_id '{}'!!!".format( layer.name(), t_id)) def download_image(self, url): res = False img = None msg = {'text': '', 'level': Qgis.Warning} if url: self.logger.info(__name__, "Downloading file from {}".format(url)) msg_status_bar = "Downloading image from document repository (this might take a while)..." with ProcessWithStatus(msg_status_bar): if is_connected(TEST_SERVER): nam = QNetworkAccessManager() request = QNetworkRequest(QUrl(url)) reply = nam.get(request) loop = QEventLoop() reply.finished.connect(loop.quit) loop.exec_() status = reply.attribute( QNetworkRequest.HttpStatusCodeAttribute) if status == 200: res = True img = reply.readAll() else: res = False msg['text'] = QCoreApplication.translate( "SettingsDialog", "There was a problem connecting to the server. The server might be down or the service cannot be reached at the given URL." ) else: res = False msg['text'] = QCoreApplication.translate( "SettingsDialog", "There was a problem connecting to Internet.") else: res = False msg['text'] = QCoreApplication.translate("SettingsDialog", "Not valid URL") if not res: self.logger.log_message(__name__, msg['text'], msg['level']) return res, img def get_plots_related_to_parcel(self, parcel_t_id): return self._ladm_data.get_plots_related_to_parcels( self._db, [parcel_t_id], None, self.plot_layer(), self.uebaunit_table()) def get_layer(self, table_name): return self.app.core.get_layer(self._db, table_name, True) def search_data_basic_info(self, **kwargs): return self._ladm_queries.get_igac_basic_info(self._db, **kwargs) def search_data_legal_info(self, **kwargs): return self._ladm_queries.get_igac_legal_info(self._db, **kwargs) def search_data_physical_info(self, **kwargs): return self._ladm_queries.get_igac_physical_info(self._db, **kwargs) def search_data_economic_info(self, **kwargs): return self._ladm_queries.get_igac_economic_info(self._db, **kwargs) def query_informal_parcels(self): """ :return: Triple --> parcel_number, current, total """ # We always go to the DB to get informality info parcel_layer = self.app.core.get_layer(self._db, self._db.names.LC_PARCEL_T, True) informal_parcel_t_ids = self._ladm_data.get_informal_parcel_tids( self._db, parcel_layer) # Overwrite cache self._informal_parcels_info = tuple() self._informal_index = -1 self._informal_parcels_len = 0 if informal_parcel_t_ids: # Get parcel info ordered by parcel number parcels = self._ladm_data.get_features_from_t_ids( self.parcel_layer(), self._db.names.T_ID_F, informal_parcel_t_ids, no_attributes=False, no_geometry=False, only_attributes=[self._db.names.LC_PARCEL_T_PARCEL_NUMBER_F], order_by=self._db.names.LC_PARCEL_T_PARCEL_NUMBER_F) # Create a tuple of lists ([t_id: parcel_number], ...) self._informal_parcels_info = tuple([ p[self._db.names.T_ID_F], p[ self._db.names.LC_PARCEL_T_PARCEL_NUMBER_F] ] for p in parcels) self._informal_parcels_len = len(self._informal_parcels_info) return self.get_next_informal_parcel() def get_next_informal_parcel(self): return self._traverse_informal_parcel_info() def get_previous_informal_parcel(self): return self._traverse_informal_parcel_info(False) def _traverse_informal_parcel_info(self, next=True): """ Get a triple corresponding to an informal parcel number, the current index and the total of parcels. Note that if we get to the end and ask for the next parcel, we start over again. Similarly. if we are in the 1st parcel and ask for the previous one, then we get the latest one. :param next: Whether we need the next parcel's info or the previous one. :return: Triple --> parcel_number, current_idx, total_parcels (the current_idx returned is for display purposes) """ if not self._informal_parcels_len: return '', 0, 0 index = self._informal_index # Get current index if next: # Now set the current index self._informal_index = index + 1 if index + 1 < self._informal_parcels_len else 0 else: # Previous self._informal_index = index - 1 if index >= 1 else self._informal_parcels_len - 1 return self._informal_parcels_info[self._informal_index][ 1], self._informal_index + 1, self._informal_parcels_len
class AbsWizardFactory(QWizard): update_wizard_is_open_flag = pyqtSignal(bool) set_finalize_geometry_creation_enabled_emitted = pyqtSignal(bool) def __init__(self, iface, db, qgis_utils, wizard_settings): super(AbsWizardFactory, self).__init__() self.iface = iface self._db = db self.qgis_utils = qgis_utils self.wizard_config = wizard_settings self.logger = Logger() self.names = self._db.names self.help_strings = HelpStrings() self.translatable_config_strings = TranslatableConfigStrings() load_ui(self.wizard_config[WIZARD_UI], self) self.WIZARD_FEATURE_NAME = self.wizard_config[WIZARD_FEATURE_NAME] self.WIZARD_TOOL_NAME = self.wizard_config[WIZARD_TOOL_NAME] self.EDITING_LAYER_NAME = self.wizard_config[WIZARD_EDITING_LAYER_NAME] self._layers = self.wizard_config[WIZARD_LAYERS] self.set_ready_only_field() self.init_gui() def init_gui(self): raise NotImplementedError def adjust_page_1_controls(self): raise NotImplementedError def finished_dialog(self): raise NotImplementedError def prepare_feature_creation(self): result = self.prepare_feature_creation_layers() if result: self.edit_feature() else: self.close_wizard(show_message=False) def prepare_feature_creation_layers(self): raise NotImplementedError def close_wizard(self, message=None, show_message=True): raise NotImplementedError def rollback_in_layers_with_empty_editing_buffer(self): for layer_name in self._layers: if self._layers[layer_name][ LAYER] is not None: # If the layer was removed, this becomes None if self._layers[layer_name][LAYER].isEditable(): if not self._layers[layer_name][LAYER].editBuffer( ).isModified(): self._layers[layer_name][LAYER].rollBack() def disconnect_signals(self): raise NotImplementedError def edit_feature(self): raise NotImplementedError def finish_feature_creation(self, layerId, features): message = self.post_save(features) self._layers[ self.EDITING_LAYER_NAME][LAYER].committedFeaturesAdded.disconnect( self.finish_feature_creation) self.logger.info( __name__, "{} committedFeaturesAdded SIGNAL disconnected".format( self.WIZARD_FEATURE_NAME)) self.close_wizard(message) def post_save(self, features): raise NotImplementedError def open_form(self, layer): raise NotImplementedError def exec_form(self, layer): feature = self.get_feature_exec_form(layer) dialog = self.iface.getFeatureForm(layer, feature) dialog.rejected.connect(self.form_rejected) dialog.setModal(True) if dialog.exec_(): self.exec_form_advanced(layer) saved = layer.commitChanges() if not saved: layer.rollBack() self.logger.warning_msg( __name__, QCoreApplication.translate( "WizardTranslations", "Error while saving changes. {} could not be created." ).format(self.WIZARD_FEATURE_NAME)) for e in layer.commitErrors(): self.logger.warning(__name__, "Commit error: {}".format(e)) else: layer.rollBack() self.iface.mapCanvas().refresh() def get_feature_exec_form(self, layer): raise NotImplementedError def exec_form_advanced(self, layer): raise NotImplementedError def form_rejected(self): message = QCoreApplication.translate( "WizardTranslations", "'{}' tool has been closed because you just closed the form." ).format(self.WIZARD_TOOL_NAME) self.close_wizard(message) def save_settings(self): settings = QSettings() settings.setValue( self.wizard_config[WIZARD_QSETTINGS] [WIZARD_QSETTINGS_LOAD_DATA_TYPE], 'create_manually' if self.rad_create_manually.isChecked() else 'refactor') def restore_settings(self): settings = QSettings() load_data_type = settings.value( self.wizard_config[WIZARD_QSETTINGS] [WIZARD_QSETTINGS_LOAD_DATA_TYPE]) or 'create_manually' if load_data_type == 'refactor': self.rad_refactor.setChecked(True) else: self.rad_create_manually.setChecked(True) def show_help(self): self.qgis_utils.show_help(self.wizard_config[WIZARD_HELP]) def set_ready_only_field(self, read_only=True): if self._layers[self.EDITING_LAYER_NAME][LAYER] is not None: for field in self.wizard_config[WIZARD_READ_ONLY_FIELDS]: # Not validate field that are read only QGISUtils.set_read_only_field( self._layers[self.EDITING_LAYER_NAME][LAYER], field, read_only)
class AppGUIInterface(QObject): add_indicators_requested = pyqtSignal( str, QgsLayerTreeNode.NodeType) # node name, node type def __init__(self, iface): QObject.__init__(self) self.iface = iface self.logger = Logger() def trigger_add_feature(self): self.iface.actionAddFeature().trigger() def trigger_vertex_tool(self): self.iface.actionVertexTool().trigger() def create_progress_message_bar(self, text, progress): progressMessageBar = self.iface.messageBar().createMessage( PLUGIN_NAME, text) progressMessageBar.layout().addWidget(progress) self.iface.messageBar().pushWidget(progressMessageBar, Qgis.Info) def refresh_layer_symbology(self, layer_id): self.iface.layerTreeView().refreshLayerSymbology(layer_id) def trigger_repaint_on_layer(self, layer): layer.triggerRepaint() def refresh_map(self): self.iface.mapCanvas().refresh() def redraw_all_layers(self): self.iface.mapCanvas().redrawAllLayers() def freeze_map(self, frozen): self.iface.mapCanvas().freeze(frozen) def activate_layer(self, layer): self.iface.layerTreeView().setCurrentLayer(layer) def set_node_visibility(self, node, visible=True): # Modes may eventually be layer_id, group_name, layer, group if node is not None: node.setItemVisibilityChecked(visible) def clear_status_bar(self): self.iface.statusBarIface().clearMessage() def add_indicators(self, node_name, node_type, payload, names): """ Adds all indicators for a node in layer tree. It searches for the proper node and its config. :param node_name: Key to get the config and possibly, the node (see payload) :param node_type: QgsLayerTreeNode.NodeType :param payload: If the node is a LADM layer, we need the layer object, as the name is not enough to disambiguate between layers from different connections :param names: DBMappingRegistry instance to read layer names from """ # First get the node node = None root = QgsProject.instance().layerTreeRoot() if node_type == QgsLayerTreeNode.NodeGroup: node = root.findGroup(node_name) elif node_type == QgsLayerTreeNode.NodeLayer: if payload: node = root.findLayer(payload) # Search by QgsMapLayer else: # Get the first layer matching the node name layers = QgsProject.instance().mapLayersByName(node_name) if layers: node = root.findLayer(layers[0]) if not node: self.logger.warning( __name__, "Node not found for adding indicators! ({}, {})".format( node_name, node_type)) return # No node, no party # Then, get the config indicators_config = LayerTreeIndicatorConfig().get_indicators_config( node_name, node_type, names) if not indicators_config: self.logger.warning( __name__, "Configuration for indicators not found for node '{}'!".format( node_name)) # And finally... for config in indicators_config: self.logger.debug( __name__, "Adding indicator for {} node '{}'...".format( 'group' if node_type == QgsLayerTreeNode.NodeGroup else 'layer', node_name)) self.add_indicator(node, config) def add_indicator(self, node, config): """ Adds a single indicator for the node, based on a config dict :param node: Layer tree node :param config: Dictionary with required data to set the indicator """ indicator = QgsLayerTreeViewIndicator(self.iface.layerTreeView()) indicator.setToolTip(config[INDICATOR_TOOLTIP]) indicator.setIcon(config[INDICATOR_ICON]) indicator.clicked.connect(config[INDICATOR_SLOT]) self.iface.layerTreeView().addIndicator(node, indicator) def set_layer_visibility(self, layer, visible): node = QgsProject.instance().layerTreeRoot().findLayer(layer.id()) self.set_node_visibility(node, visible) @pyqtSlot() def clear_message_bar(self): self.iface.messageBar().clearWidgets() def zoom_full(self): self.iface.zoomFull() def zoom_to_active_layer(self): self.iface.zoomToActiveLayer() def zoom_to_selected(self): self.iface.actionZoomToSelected().trigger() def zoom_to_feature_ids(self, layer, fids): self.iface.mapCanvas().zoomToFeatureIds(layer, fids) def zoom_to_extent(self, extent): self.iface.mapCanvas().zoomToFeatureExtent(extent) def show_message(self, msg, level, duration=5): self.clear_message_bar( ) # Remove previous messages before showing a new one self.iface.messageBar().pushMessage("Asistente LADM-COL", msg, level, duration) def show_status_bar_message(self, msg, duration): self.iface.statusBarIface().showMessage(msg, duration) def add_tabified_dock_widget(self, area, dock_widget): """ Adds the dock_widget to the given area, making sure it is tabified if other dock widgets exist. :param area: Value of the Qt.DockWidgetArea enum :param dock_widget: QDockWidget object """ self.iface.addTabifiedDockWidget(area, dock_widget, raiseTab=True) def open_feature_form(self, layer, feature): self.iface.openFeatureForm(layer, feature) def flash_features(self, layer, fids, flashes=1, duration=500): self.iface.mapCanvas().flashFeatureIds(layer, fids, QColor(255, 0, 0, 255), QColor(255, 0, 0, 0), flashes=flashes, duration=duration)
class SettingsDialog(QDialog, DIALOG_UI): """ Customizable dialog to configure LADM-COL Assistant. It can be created passing a SettingsContext with specific params or it can be instantiated and then set params one by one. """ db_connection_changed = pyqtSignal(DBConnector, bool, str) # dbconn, ladm_col_db, source open_dlg_import_schema = pyqtSignal( Context) # Context for the import schema dialog def __init__(self, conn_manager=None, context=None, parent=None): QDialog.__init__(self, parent) self.setupUi(self) self.parent = parent self.logger = Logger() self.conn_manager = conn_manager self.app = AppInterface() self.sbx_tolerance.setMaximum(TOLERANCE_MAX_VALUE) self._valid_document_repository = False # Needs to be True if users want to enable doc repo (using test button) context = context if context else SettingsContext() self.db_source = context.db_source # default db source is COLLECTED_DB_SOURCE self._required_models = context.required_models self._tab_pages_list = context.tab_pages_list self._blocking_mode = context.blocking_mode # Whether the dialog can only be accepted on valid DB connections or not self._action_type = context.action_type # By default "config" self.setWindowTitle(context.title) self._db = None self.init_db_engine = None self.dbs_supported = ConfigDBsSupported() self._open_dlg_import_schema = False # After accepting, if non-valid DB is configured, we can go to import schema self.online_models_radio_button.setEnabled( False) # This option is disabled until we have online models back! self.online_models_radio_button.setChecked(True) self.online_models_radio_button.toggled.connect( self.model_provider_toggle) self.custom_model_directories_line_edit.setText("") self.custom_models_dir_button.clicked.connect( self.show_custom_model_dir) self.custom_model_directories_line_edit.setVisible(False) self.custom_models_dir_button.setVisible(False) # Set connections self.buttonBox.accepted.disconnect() self.buttonBox.accepted.connect(self.accepted) self.buttonBox.helpRequested.connect(self.show_help) self.finished.connect(self.finished_slot) self.btn_test_connection.clicked.connect(self.test_connection) self.btn_test_ladm_col_structure.clicked.connect( self.test_ladm_col_structure) self.btn_test_service.clicked.connect(self.test_service) self.btn_test_service_transitional_system.clicked.connect( self.test_service_transitional_system) self.txt_service_endpoint.textEdited.connect( self.source_service_endpoint_changed) # For manual changes only self.btn_default_value_sources.clicked.connect( self.set_default_value_source_service) self.btn_default_value_transitional_system.clicked.connect( self.set_default_value_transitional_system_service) log_file_selector = make_save_file_selector( self.txt_log_file_path, title=QCoreApplication.translate("SettingsDialog", "Create log file"), file_filter=QCoreApplication.translate("SettingsDialog", "Plain text (*.txt)"), extension='.txt') self.btn_log_file_path.clicked.connect(log_file_selector) self.chk_use_roads.toggled.connect(self.update_images_state) self.bar = QgsMessageBar() self.bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.layout().addWidget(self.bar, 0, 0, Qt.AlignTop) self.cbo_db_engine.clear() self._lst_db = self.dbs_supported.get_db_factories() self._lst_panel = dict() for key, value in self._lst_db.items(): self.cbo_db_engine.addItem(value.get_name(), key) self._lst_panel[key] = value.get_config_panel(self) self._lst_panel[key].notify_message_requested.connect( self.show_message) self.db_layout.addWidget(self._lst_panel[key]) self.db_engine_changed() # Trigger some default behaviours self.restore_db_source_settings( ) # restore settings with default db source self.restore_settings() self.roles = RoleRegistry() self.load_roles() self.cbo_db_engine.currentIndexChanged.connect(self.db_engine_changed) self.rejected.connect(self.close_dialog) self._update_tabs() if context.tip: self.show_tip(context.tip) def set_db_source(self, db_source): self.db_source = db_source self.restore_db_source_settings() def set_tab_pages_list(self, tab_pages_list): self._tab_pages_list = tab_pages_list self._update_tabs() def set_required_models(self, required_models): self._required_models = required_models def set_blocking_mode(self, block): self._blocking_mode = block def _update_tabs(self): """ Show only those tabs that are listed in tab_pages_list, if any. If it's an empty list, show all tabs. """ if self._tab_pages_list: for i in reversed(range(self.tabWidget.count())): if i not in self._tab_pages_list: self.tabWidget.removeTab(i) def get_models_for_test_connection(self): """ If required models have been set from context or via set_required_models(), we respect them, they have the priority for the tests we do. Otherwise, if selected role is not the current active role, we'll test if the DB has the models that such a selected role supports. Because if we test against the active role, we'll might say sth wrong to the user, like: Your current DB connection has no LADM-COL models, just because the user is about to change the role from one that does not have interest in the DB by one which is the proper one for the DB. Specially useful for Add-ons that add new roles with access to specific models. Note we don't modify the member variable at all, otherwise subsequent calls to this method will never get into the first if clause. :return: A dict of required key models or of role_supported and role_hidden model keys. """ models = { REQUIRED_MODELS: self._required_models } if self._required_models else dict() if not models: selected_role = self.get_selected_role() if self.roles.get_active_role() != selected_role: models = self.roles.get_role_models(selected_role) return models def load_roles(self): """ Initialize group box for selecting the active role """ self.gbx_active_role_layout = QVBoxLayout() dict_roles = self.roles.get_roles_info() checked = False active_role = self.roles.get_active_role() # Initialize radio buttons for k, v in dict_roles.items(): radio = QRadioButton(v) radio.setToolTip(self.roles.get_role_description(k)) if not checked: if k == active_role: radio.setChecked(True) checked = True self.gbx_active_role_layout.addWidget(radio) self.gbx_active_role.setLayout(self.gbx_active_role_layout) def close_dialog(self): self.close() def showEvent(self, event): # It is necessary to reload the variables # to load the database and schema name self.restore_settings() self.btn_test_ladm_col_structure.setVisible( self._action_type != EnumDbActionType.SCHEMA_IMPORT) def model_provider_toggle(self): if self.offline_models_radio_button.isChecked(): self.custom_model_directories_line_edit.setVisible(True) self.custom_models_dir_button.setVisible(True) else: self.custom_model_directories_line_edit.setVisible(False) self.custom_models_dir_button.setVisible(False) self.custom_model_directories_line_edit.setText("") def _get_db_connector_from_gui(self): current_db_engine = self.cbo_db_engine.currentData() params = self._lst_panel[current_db_engine].read_connection_parameters( ) db = self._lst_db[current_db_engine].get_db_connector(params) return db def get_db_connection(self): if self._db is not None: self.logger.info(__name__, "Returning existing db connection...") else: self.logger.info(__name__, "Getting new db connection...") self._db = self._get_db_connector_from_gui() self._db.open_connection() return self._db def show_custom_model_dir(self): dlg = CustomModelDirDialog( self.custom_model_directories_line_edit.text(), self) dlg.exec_() def accepted(self): """ We start checking the document repository configuration and only allow to continue if we have a valid repo or if the repo won't be used. Then, check if connection to DB/schema is valid, if not, block the dialog. If valid, check it complies with LADM. If not, block the dialog. If it complies, we have two options: To emit db_connection changed or not. Finally, we store options in QSettings. """ res_doc_repo, msg_doc_repo = self.check_document_repository_before_saving_settings( ) if not res_doc_repo: self.show_message(msg_doc_repo, Qgis.Warning, 0) return # Do not close the dialog if self.chk_debug.isChecked( ) and not self.txt_log_file_path.text().strip(): self.show_message( QCoreApplication.translate( "SettingsDialog", "If the debug is enabled, a log file path has to be set."), Qgis.Warning, 0) return # Do not close the dialog ladm_col_schema = False db = self._get_db_connector_from_gui() test_level = EnumTestLevel.DB_SCHEMA if self._action_type == EnumDbActionType.SCHEMA_IMPORT: # Limit the validation (used in GeoPackage) test_level |= EnumTestLevel.SCHEMA_IMPORT res, code, msg = db.test_connection( test_level) # No need to pass models, we don't test that much if res: if self._action_type != EnumDbActionType.SCHEMA_IMPORT: # Only check LADM-schema if we are not in an SCHEMA IMPORT. # We know in an SCHEMA IMPORT, at this point the schema is still not LADM. ladm_col_schema, code, msg = db.test_connection( EnumTestLevel.LADM, models=self.get_models_for_test_connection()) if not ladm_col_schema: self.logger.warning( __name__, "Test connection failed! Details: {}".format(msg)) if not ladm_col_schema and self._action_type != EnumDbActionType.SCHEMA_IMPORT: if self._blocking_mode: self.show_message(msg, Qgis.Warning) return # Do not close the dialog else: if self._blocking_mode: self.show_message(msg, Qgis.Warning) return # Do not close the dialog # Connection is valid and complies with LADM current_db_engine = self.cbo_db_engine.currentData() if self._lst_panel[current_db_engine].state_changed( ) or self.init_db_engine != current_db_engine: # Emit db_connection_changed if self._db is not None: self._db.close_connection() self._db = db # Update db connect with new db conn self.conn_manager.set_db_connector_for_source( self._db, self.db_source) # Emmit signal when db source changes self.db_connection_changed.emit(self._db, ladm_col_schema, self.db_source) self.logger.debug( __name__, "Settings dialog emitted a db_connection_changed.") if not ladm_col_schema and self._action_type == EnumDbActionType.CONFIG: msg_box = QMessageBox(self) msg_box.setIcon(QMessageBox.Question) msg_box.setText( QCoreApplication.translate( "SettingsDialog", "No LADM-COL DB has been configured! You'll continue with limited functionality until you configure a LADM-COL DB with models supported by the active role.\n\nDo you want to go to 'Create LADM-COL structure' dialog?" )) msg_box.setWindowTitle( QCoreApplication.translate("SettingsDialog", "Important")) msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.Ignore) msg_box.setDefaultButton(QMessageBox.Ignore) msg_box.button(QMessageBox.Yes).setText( QCoreApplication.translate("SettingsDialog", "Yes, go to create structure")) msg_box.button(QMessageBox.Ignore).setText( QCoreApplication.translate("SettingsDialog", "No, I'll do it later")) reply = msg_box.exec_() if reply == QMessageBox.Yes: self._open_dlg_import_schema = True # We will open it when we've closed this Settings dialog # If active role is changed (a check and confirmation may be needed), refresh the GUI # Note: Better to leave this check as the last one in the accepted() method. selected_role = self.get_selected_role() if self.roles.get_active_role() != selected_role: b_change_role = True if STSession().is_user_logged(): reply = QMessageBox.question( self.parent, QCoreApplication.translate("SettingsDialog", "Warning"), QCoreApplication.translate( "SettingsDialog", "You have a ST connection opened and you want to change your role.\nIf you confirm that you want to change your role, you'll be logged out from the ST.\n\nDo you really want to change your role?" ), QMessageBox.Yes | QMessageBox.Cancel, QMessageBox.Cancel) if reply == QMessageBox.Yes: STSession().logout() elif reply == QMessageBox.Cancel: # No need to switch back selected role, the Settings Dialog gets it from role registry b_change_role = False if b_change_role: self.logger.info( __name__, "The active role has changed from '{}' to '{}'.".format( self.roles.get_active_role(), selected_role)) self.roles.set_active_role( selected_role ) # Emits signal that refreshed the plugin for this role self.save_settings(db) QDialog.accept(self) self.close() if self._open_dlg_import_schema: # After Settings dialog has been closed, we could call Import Schema depending on user's answer above self.open_dlg_import_schema.emit(Context()) self.logger.debug( __name__, "Settings dialog emitted a show Import Schema dialog.") def check_document_repository_before_saving_settings(self): # Check if source service is checked (active). If so, check if either status or endpoint changed. If so, # check if self._valid_document_repository is False. If so, we need to test the service and only allow to save # if such test is successful. res, msg = True, '' if self.connection_box.isChecked( ): # The user wants to have the source service enabled initial_config = QSettings().value( 'Asistente-LADM-COL/sources/use_service', DEFAULT_USE_SOURCE_SERVICE_SETTING, bool) initial_endpoint = QSettings().value( 'Asistente-LADM-COL/sources/service_endpoint', DEFAULT_ENDPOINT_SOURCE_SERVICE) # Config changed or Endpoint changed? if initial_config != self.connection_box.isChecked( ) or initial_endpoint.strip() != self.txt_service_endpoint.text( ).strip(): if not self._valid_document_repository: # A test service has not been run, so we need to do it now self.logger.debug( __name__, "The user wants to enable the source service but the 'test service' has not been run on the current URL. Testing it..." ) res_test, msg_test = self.test_service() if not res_test: res = False msg = QCoreApplication.translate( "SettingsDialog", "The source service is not valid, so it cannot be activated! Adjust such configuration before saving settings." ) return res, msg def source_service_endpoint_changed(self, new_text): # Source service endpoint was changed, so a test_service is required to make the valid variable True self._valid_document_repository = False def get_selected_role(self): selected_role = None radio_checked = None for i in range(self.gbx_active_role_layout.count()): radio = self.gbx_active_role_layout.itemAt(i).widget() if radio.isChecked(): radio_checked = radio.text() break for k, v in self.roles.get_roles_info().items(): if v == radio_checked: selected_role = k break return selected_role # Role key def reject(self): self.done(0) def finished_slot(self, result): self.bar.clearWidgets() def save_settings(self, db): settings = QSettings() current_db_engine = self.cbo_db_engine.currentData() settings.setValue( 'Asistente-LADM-COL/db/{db_source}/db_connection_engine'.format( db_source=self.db_source), current_db_engine) dict_conn = self._lst_panel[ current_db_engine].read_connection_parameters() self._lst_db[current_db_engine].save_parameters_conn( dict_conn=dict_conn, db_source=self.db_source) self.app.settings.custom_models = self.offline_models_radio_button.isChecked( ) if self.offline_models_radio_button.isChecked(): self.app.settings.custom_model_dirs = self.custom_model_directories_line_edit.text( ) self.app.settings.tolerance = self.sbx_tolerance.value() settings.setValue('Asistente-LADM-COL/quality/use_roads', self.chk_use_roads.isChecked()) settings.setValue( 'Asistente-LADM-COL/models/validate_data_importing_exporting', self.chk_validate_data_importing_exporting.isChecked()) settings.setValue('Asistente-LADM-COL/models/debug', self.chk_debug.isChecked()) settings.setValue('Asistente-LADM-COL/models/log_file_path', self.txt_log_file_path.text()) endpoint_transitional_system = self.txt_service_transitional_system.text( ).strip() settings.setValue( 'Asistente-LADM-COL/sources/service_transitional_system', (endpoint_transitional_system[:-1] if endpoint_transitional_system.endswith('/') else endpoint_transitional_system) or TransitionalSystemConfig().ST_DEFAULT_DOMAIN) settings.setValue('Asistente-LADM-COL/sources/use_service', self.connection_box.isChecked()) endpoint = self.txt_service_endpoint.text().strip() settings.setValue( 'Asistente-LADM-COL/sources/service_endpoint', (endpoint[:-1] if endpoint.endswith('/') else endpoint) or DEFAULT_ENDPOINT_SOURCE_SERVICE) settings.setValue( 'Asistente-LADM-COL/automatic_values/automatic_values_in_batch_mode', self.chk_automatic_values_in_batch_mode.isChecked()) # Changes in automatic namespace, local_id or t_ili_tid configuration? current_namespace_enabled = settings.value( 'Asistente-LADM-COL/automatic_values/namespace_enabled', True, bool) current_namespace_prefix = settings.value( 'Asistente-LADM-COL/automatic_values/namespace_prefix', "") current_local_id_enabled = settings.value( 'Asistente-LADM-COL/automatic_values/local_id_enabled', True, bool) current_t_ili_tid_enabled = settings.value( 'Asistente-LADM-COL/automatic_values/t_ili_tid_enabled', True, bool) settings.setValue( 'Asistente-LADM-COL/automatic_values/namespace_enabled', self.namespace_collapsible_group_box.isChecked()) if self.namespace_collapsible_group_box.isChecked(): settings.setValue( 'Asistente-LADM-COL/automatic_values/namespace_prefix', self.txt_namespace.text()) settings.setValue( 'Asistente-LADM-COL/automatic_values/local_id_enabled', self.chk_local_id.isChecked()) settings.setValue( 'Asistente-LADM-COL/automatic_values/t_ili_tid_enabled', self.chk_t_ili_tid.isChecked()) if current_namespace_enabled != self.namespace_collapsible_group_box.isChecked() or \ current_namespace_prefix != self.txt_namespace.text() or \ current_local_id_enabled != self.chk_local_id.isChecked() or \ current_t_ili_tid_enabled != self.chk_t_ili_tid.isChecked(): if db is not None: self.logger.info( __name__, "Automatic values changed in Settings dialog. All LADM-COL layers are being updated..." ) self.app.core.automatic_fields_settings_changed(db) def restore_db_source_settings(self): settings = QSettings() default_db_engine = self.dbs_supported.id_default_db self.init_db_engine = settings.value( 'Asistente-LADM-COL/db/{db_source}/db_connection_engine'.format( db_source=self.db_source), default_db_engine) index_db_engine = self.cbo_db_engine.findData(self.init_db_engine) if index_db_engine == -1: index_db_engine = self.cbo_db_engine.findData(default_db_engine) self.cbo_db_engine.setCurrentIndex(index_db_engine) self.db_engine_changed() # restore db settings for all panels for db_engine, db_factory in self._lst_db.items(): dict_conn = db_factory.get_parameters_conn(self.db_source) self._lst_panel[db_engine].write_connection_parameters(dict_conn) self._lst_panel[db_engine].save_state() def restore_settings(self): # Restore QSettings settings = QSettings() custom_model_directories_is_checked = self.app.settings.custom_models if custom_model_directories_is_checked: self.offline_models_radio_button.setChecked(True) self.custom_model_directories_line_edit.setText( self.app.settings.custom_model_dirs) self.custom_model_directories_line_edit.setVisible(True) self.custom_models_dir_button.setVisible(True) else: self.online_models_radio_button.setChecked(True) self.custom_model_directories_line_edit.setText("") self.custom_model_directories_line_edit.setVisible(False) self.custom_models_dir_button.setVisible(False) self.sbx_tolerance.setValue(self.app.settings.tolerance) use_roads = settings.value('Asistente-LADM-COL/quality/use_roads', True, bool) self.chk_use_roads.setChecked(use_roads) self.update_images_state(use_roads) self.chk_automatic_values_in_batch_mode.setChecked( settings.value( 'Asistente-LADM-COL/automatic_values/automatic_values_in_batch_mode', DEFAULT_AUTOMATIC_VALUES_IN_BATCH_MODE, bool)) self.connection_box.setChecked( settings.value('Asistente-LADM-COL/sources/use_service', DEFAULT_USE_SOURCE_SERVICE_SETTING, bool)) self.namespace_collapsible_group_box.setChecked( settings.value( 'Asistente-LADM-COL/automatic_values/namespace_enabled', True, bool)) self.chk_local_id.setChecked( settings.value( 'Asistente-LADM-COL/automatic_values/local_id_enabled', True, bool)) self.chk_t_ili_tid.setChecked( settings.value( 'Asistente-LADM-COL/automatic_values/t_ili_tid_enabled', True, bool)) self.txt_namespace.setText( str( settings.value( 'Asistente-LADM-COL/automatic_values/namespace_prefix', ""))) self.chk_validate_data_importing_exporting.setChecked( settings.value( 'Asistente-LADM-COL/models/validate_data_importing_exporting', True, bool)) self.chk_debug.setChecked( settings.value('Asistente-LADM-COL/models/debug', DEFAULT_ILI2DB_DEBUG_MODE, bool)) self.txt_log_file_path.setText( settings.value('Asistente-LADM-COL/models/log_file_path', '')) self.txt_service_transitional_system.setText( settings.value( 'Asistente-LADM-COL/sources/service_transitional_system', TransitionalSystemConfig().ST_DEFAULT_DOMAIN)) self.txt_service_endpoint.setText( settings.value('Asistente-LADM-COL/sources/service_endpoint', DEFAULT_ENDPOINT_SOURCE_SERVICE)) def db_engine_changed(self): if self._db is not None: self._db.close_connection() self._db = None # Reset db connection for key, value in self._lst_panel.items(): value.setVisible(False) current_db_engine = self.cbo_db_engine.currentData() self._lst_panel[current_db_engine].setVisible(True) def test_connection(self): db = self._get_db_connector_from_gui() test_level = EnumTestLevel.DB_SCHEMA if self._action_type == EnumDbActionType.SCHEMA_IMPORT: test_level |= EnumTestLevel.SCHEMA_IMPORT res, code, msg = db.test_connection( test_level) # No need to pass models, we don't test that much if db is not None: db.close_connection() self.show_message(msg, Qgis.Info if res else Qgis.Warning) self.logger.info(__name__, "Test connection!") self.logger.debug( __name__, "Test connection ({}): {}, {}".format(test_level, res, msg)) def test_ladm_col_structure(self): db = self._get_db_connector_from_gui() res, code, msg = db.test_connection( test_level=EnumTestLevel.LADM, models=self.get_models_for_test_connection()) if db is not None: db.close_connection() self.show_message(msg, Qgis.Info if res else Qgis.Warning) self.logger.info(__name__, "Test LADM structure!") self.logger.debug( __name__, "Test connection ({}): {}, {}".format(EnumTestLevel.LADM, res, msg)) def test_service(self): self.setEnabled(False) QCoreApplication.processEvents() res, msg = self.app.core.is_source_service_valid( self.txt_service_endpoint.text().strip()) self._valid_document_repository = res # Required to be True if the user wants to enable the source service self.setEnabled(True) self.show_message(msg['text'], msg['level'], 0) return res, msg def test_service_transitional_system(self): self.setEnabled(False) QCoreApplication.processEvents() res, msg = self.app.core.is_transitional_system_service_valid( self.txt_service_transitional_system.text().strip()) self.setEnabled(True) self.show_message(msg['text'], msg['level']) def set_default_value_source_service(self): self.txt_service_endpoint.setText(DEFAULT_ENDPOINT_SOURCE_SERVICE) def set_default_value_transitional_system_service(self): self.txt_service_transitional_system.setText( TransitionalSystemConfig().ST_DEFAULT_DOMAIN) def show_message(self, message, level, duration=10): self.bar.clearWidgets( ) # Remove previous messages before showing a new one self.bar.pushMessage(message, level, duration) def show_tip(self, tip): self.show_message(tip, Qgis.Info, 0) # Don't show counter for the tip message def update_images_state(self, checked): self.img_with_roads.setEnabled(checked) self.img_with_roads.setToolTip( QCoreApplication.translate( "SettingsDialog", "Missing roads will be marked as errors." ) if checked else '') self.img_without_roads.setEnabled(not checked) self.img_without_roads.setToolTip( '' if checked else QCoreApplication.translate( "SettingsDialog", "Missing roads will not be marked as errors." )) def show_help(self): show_plugin_help("settings") def set_action_type(self, action_type): self._action_type = action_type for key, value in self._lst_panel.items(): value.set_action(action_type)
class AppGUIInterface(QObject): add_indicators_requested = pyqtSignal( str, QgsLayerTreeNode.NodeType) # node name, node type def __init__(self, iface): QObject.__init__(self) self.iface = iface self.logger = Logger() def trigger_add_feature(self): self.iface.actionAddFeature().trigger() def trigger_vertex_tool(self): self.iface.actionVertexTool().trigger() def create_progress_message_bar(self, text, progress): progressMessageBar = self.iface.messageBar().createMessage( PLUGIN_NAME, text) progressMessageBar.layout().addWidget(progress) self.iface.messageBar().pushWidget(progressMessageBar, Qgis.Info) def refresh_layer_symbology(self, layer_id): self.iface.layerTreeView().refreshLayerSymbology(layer_id) def refresh_map(self): self.iface.mapCanvas().refresh() def redraw_all_layers(self): self.iface.mapCanvas().redrawAllLayers() def freeze_map(self, frozen): self.iface.mapCanvas().freeze(frozen) def activate_layer(self, layer): self.iface.layerTreeView().setCurrentLayer(layer) def set_node_visibility(self, node, visible=True): # Modes may eventually be layer_id, group_name, layer, group if node is not None: node.setItemVisibilityChecked(visible) def remove_error_group(self): group = self.get_error_layers_group() parent = group.parent() parent.removeChildNode(group) def clear_status_bar(self): self.iface.statusBarIface().clearMessage() def add_error_layer(self, db, error_layer): group = self.get_error_layers_group() # Check if layer is loaded and remove it layers = group.findLayers() for layer in layers: if layer.name() == error_layer.name(): group.removeLayer(layer.layer()) break added_layer = QgsProject.instance().addMapLayer(error_layer, False) index = QgisModelBakerUtils().get_suggested_index_for_layer( added_layer, group) added_layer = group.insertLayer(index, added_layer).layer() if added_layer.isSpatial(): # db connection is none because we are using a memory layer SymbologyUtils().set_layer_style_from_qml(db, added_layer, is_error_layer=True) if isinstance(added_layer.renderer(), QgsCategorizedSymbolRenderer): # Remove empty style categories as they just make difficult to understand validation errors unique_values = added_layer.uniqueValues( added_layer.fields().indexOf( QCoreApplication.translate("QualityRule", "codigo_error"))) renderer = added_layer.renderer() for cat in reversed(renderer.categories() ): # To be safe while removing categories if cat.value() not in unique_values: renderer.deleteCategory( renderer.categoryIndexForValue(cat.value())) added_layer.setRenderer(added_layer.renderer().clone()) return added_layer def get_error_layers_group(self): """ Get the topology errors group. If it exists but is placed in another position rather than the top, it moves the group to the top. """ root = QgsProject.instance().layerTreeRoot() translated_strings = TranslatableConfigStrings.get_translatable_config_strings( ) group = root.findGroup(translated_strings[ERROR_LAYER_GROUP]) if group is None: group = root.insertGroup(0, translated_strings[ERROR_LAYER_GROUP]) self.add_indicators_requested.emit( translated_strings[ERROR_LAYER_GROUP], QgsLayerTreeNode.NodeGroup) elif not self.iface.layerTreeView().layerTreeModel().node2index( group).row() == 0 or type(group.parent()) is QgsLayerTreeGroup: group_clone = group.clone() root.insertChildNode(0, group_clone) parent = group.parent() parent.removeChildNode(group) group = group_clone return group def add_indicators(self, node_name, node_type, payload): """ Adds all indicators for a node in layer tree. It searches for the proper node and its config. :param node_name: Key to get the config and possibly, the node (see payload) :param node_type: QgsLayerTreeNode.NodeType :param payload: If the node is a LADM layer, we need the layer object, as the name is not enough to disambiguate between layers from different connections """ # First get the node node = None root = QgsProject.instance().layerTreeRoot() if node_type == QgsLayerTreeNode.NodeGroup: node = root.findGroup(node_name) elif node_type == QgsLayerTreeNode.NodeLayer: if payload: node = root.findLayer(payload) # Search by QgsMapLayer else: # Get the first layer matching the node name layers = QgsProject.instance().mapLayersByName(node_name) if layers: node = root.findLayer(layers[0]) if not node: self.logger.warning( __name__, "Node not found for adding indicators! ({}, {})".format( node_name, node_type)) return # No node, no party # Then, get the config indicators_config = LayerTreeIndicatorConfig().get_indicators_config( node_name, node_type) if not indicators_config: self.logger.warning( __name__, "Configuration for indicators not found for node '{}'!".format( node_name)) # And finally... for config in indicators_config: self.add_indicator(node, config) def add_indicator(self, node, config): """ Adds a single indicator for the node, based on a config dict :param node: Layer tree node :param config: Dictionary with required data to set the indicator """ indicator = QgsLayerTreeViewIndicator(self.iface.layerTreeView()) indicator.setToolTip(config[INDICATOR_TOOLTIP]) indicator.setIcon(config[INDICATOR_ICON]) indicator.clicked.connect(config[INDICATOR_SLOT]) self.iface.layerTreeView().addIndicator(node, indicator) def export_error_group(self): """Exports the error group to GeoPackage""" group = self.get_error_layers_group() if group: layers = group.findLayerIds() if not layers: self.logger.warning_msg( __name__, QCoreApplication.translate( "AppGUIInterface", "There are no error layers to export!")) return filename, matched_filter = QFileDialog.getSaveFileName( self.iface.mainWindow(), QCoreApplication.translate( "AppGUIInterface", "Where do you want to save your GeoPackage?"), ".", QCoreApplication.translate("AppGUIInterface", "GeoPackage (*.gpkg)")) if filename: if not filename.endswith(".gpkg") and filename: filename = filename + ".gpkg" feedback = CustomFeedbackWithErrors() try: msg = QCoreApplication.translate( "AppGUIInterface", "Exporting quality errors to GeoPackage...") with ProcessWithStatus(msg): processing.run("native:package", { 'LAYERS': layers, 'OUTPUT': filename, 'OVERWRITE': False, 'SAVE_STYLES': True }, feedback=feedback) except QgsProcessingException as e: self.logger.warning_msg( __name__, QCoreApplication.translate( "AppGUIInterface", "The quality errors could not be exported. Details: {}" .format(feedback.msg))) return self.logger.success_msg( __name__, QCoreApplication.translate( "AppGUIInterface", "The quality errors have been exported to GeoPackage!") ) else: self.logger.warning_msg( __name__, QCoreApplication.translate( "AppGUIInterface", "Export to GeoPackage was cancelled. No output file was selected." ), 5) else: self.logger.warning_msg( __name__, QCoreApplication.translate( "AppGUIInterface", "There is no quality error group to export!"), 5) def set_error_group_visibility(self, visible): self.set_node_visibility(self.get_error_layers_group(), visible) def set_layer_visibility(self, layer, visible): node = QgsProject.instance().layerTreeRoot().findLayer(layer.id()) self.set_node_visibility(node, visible) def error_group_exists(self): root = QgsProject.instance().layerTreeRoot() translated_strings = TranslatableConfigStrings.get_translatable_config_strings( ) return root.findGroup( translated_strings[ERROR_LAYER_GROUP]) is not None @pyqtSlot() def clear_message_bar(self): self.iface.messageBar().clearWidgets() def zoom_full(self): self.iface.zoomFull() def zoom_to_active_layer(self): self.iface.zoomToActiveLayer() def zoom_to_selected(self): self.iface.actionZoomToSelected().trigger() def show_message(self, msg, level, duration=5): self.clear_message_bar( ) # Remove previous messages before showing a new one self.iface.messageBar().pushMessage("Asistente LADM-COL", msg, level, duration) def show_status_bar_message(self, msg, duration): self.iface.statusBarIface().showMessage(msg, duration) def add_tabified_dock_widget(self, area, dock_widget): """ Adds the dock_widget to the given area, making sure it is tabified if other dock widgets exist. :param area: Value of the Qt.DockWidgetArea enum :param dock_widget: QDockWidget object """ if Qgis.QGIS_VERSION_INT >= 31300: # Use native addTabifiedDockWidget self.iface.addTabifiedDockWidget(area, dock_widget, raiseTab=True) else: # Use plugin's addTabifiedDockWidget, which does not raise the new tab dock_widgets = list() for dw in self.iface.mainWindow().findChildren(QDockWidget): if dw.isVisible() and self.iface.mainWindow().dockWidgetArea( dw) == area: dock_widgets.append(dw) self.iface.mainWindow().addDockWidget( area, dock_widget) # We add the dock widget, then attempt to tabify if dock_widgets: self.logger.debug( __name__, "Tabifying dock widget {}...".format( dock_widget.windowTitle())) self.iface.mainWindow().tabifyDockWidget( dock_widgets[0], dock_widget) # No way to prefer one Dock Widget
class STTaskSteps(QObject): """ Manage task steps """ def __init__(self, task): QObject.__init__(self) self.task = task self.task_id = task.get_id() self.task_type = task.get_type() self.logger = Logger() self.__steps = list() self.task_steps_config = TaskStepsConfig() self.__initialize_steps() def __initialize_steps(self): """ Get actions from task step config and create STTaskStep objects for the task :return: List of steps ready to use """ steps_data = self.task_steps_config.get_steps_config(self.task) self.logger.info( __name__, "{} steps found for task id {}!".format(len(steps_data), self.task_id)) for step_data in steps_data: step = STTaskStep(step_data) if step.is_valid(): self.__steps.append(step) else: self.logger.error_msg( __name__, QCoreApplication.translate( "STTaskSteps", "The step '{} ({})' for the task '{} ({})' is invalid!" ).format(step.get_name(), step.get_id(), self.task.get_name(), self.task.get_id())) self.load_status(self.task_id) # Update status if found in QSettings def get_steps(self): return self.__steps def get_step(self, id): for step in self.__steps: if step.get_id() == id: return step self.logger.warning(__name__, "Step '{}' not found!".format(id)) return None def steps_complete(self): """ :return: boolean --> Are all steps done? """ for step in self.__steps: if not step.get_status(): return False return True def steps_started(self): """ :return: boolean --> Whether at least one step is done or not """ count = 0 for step in self.__steps: if step.get_status(): count += 1 return count > 0 # and count < len(self.__steps) def save_status(self, task_id, steps_status): """ Save status in QSettings :param task_id: Id of the task. :param steps_status: dict --> {step number: boolean status} """ if steps_status: self.logger.debug( __name__, "Saving step status for task ({}): {}".format( task_id, steps_status)) QSettings().setValue( "Asistente-LADM-COL/transitional_system/tasks/{}/step_status". format(task_id), json.dumps(steps_status)) for i, step in enumerate(self.__steps): index = i + 1 if index in steps_status: step.set_status(steps_status[index]) def load_status(self, task_id): """ Load status from QSettings """ try: status = json.loads(QSettings().value( "Asistente-LADM-COL/transitional_system/tasks/{}/step_status". format(task_id), "{}")) except TypeError as e: # The QSettings value is not in the format we expect, just reset it QSettings().setValue( "Asistente-LADM-COL/transitional_system/tasks/{}/step_status". format(task_id), "{}") return if status: self.logger.debug( __name__, "Loading step status for task ({}): {}".format( task_id, status)) for i, step in enumerate(self.__steps): index = str(i + 1) if index in status: step.set_status(status[index])
class GeometryUtils(QObject): def __init__(self): QObject.__init__(self) self.logger = Logger() def get_pair_boundary_plot(self, boundary_layer, plot_layer, id_field, use_selection=True): id_field_idx = plot_layer.fields().indexFromName(id_field) request = QgsFeatureRequest().setSubsetOfAttributes([id_field_idx]) polygons = plot_layer.getSelectedFeatures( request) if use_selection else plot_layer.getFeatures(request) intersect_more_pairs = list() intersect_less_pairs = list() if boundary_layer.featureCount() == 0: return (intersect_more_pairs, intersect_less_pairs) id_field_idx = boundary_layer.fields().indexFromName(id_field) request = QgsFeatureRequest().setSubsetOfAttributes([id_field_idx]) dict_features = { feature.id(): feature for feature in boundary_layer.getFeatures(request) } index = QgsSpatialIndex(boundary_layer) candidate_features = None for polygon in polygons: bbox = polygon.geometry().boundingBox() bbox.scale(1.001) candidates_ids = index.intersects(bbox) candidate_features = [ dict_features[candidate_id] for candidate_id in candidates_ids ] for candidate_feature in candidate_features: polygon_geom = polygon.geometry() is_multipart = polygon_geom.isMultipart() candidate_geometry = candidate_feature.geometry() if polygon_geom.intersects(candidate_geometry): # Does the current multipolygon have inner rings? has_inner_rings = False multi_polygon = None single_polygon = None if is_multipart: multi_polygon = polygon_geom.get() for part in range(multi_polygon.numGeometries()): if multi_polygon.ringCount(part) > 1: has_inner_rings = True break else: single_polygon = polygon_geom.get() if single_polygon.numInteriorRings() > 0: has_inner_rings = True # Now we'll test intersections against borders if has_inner_rings: # In this case we need to identify whether the # intersection is with outer rings (goes to MOREBFS # table) or with inner rings (goes to LESS table) multi_outer_rings = QgsMultiLineString() multi_inner_rings = QgsMultiLineString() if is_multipart and multi_polygon: for i in range(multi_polygon.numGeometries()): temp_polygon = multi_polygon.geometryN(i) multi_outer_rings.addGeometry( temp_polygon.exteriorRing().clone()) for j in range( temp_polygon.numInteriorRings()): multi_inner_rings.addGeometry( temp_polygon.interiorRing(j).clone()) elif not is_multipart and single_polygon: multi_outer_rings.addGeometry( single_polygon.exteriorRing().clone()) for j in range(single_polygon.numInteriorRings()): multi_inner_rings.addGeometry( single_polygon.interiorRing(j).clone()) intersection_type = QgsGeometry( multi_outer_rings).intersection( candidate_geometry).type() if intersection_type == QgsWkbTypes.LineGeometry: intersect_more_pairs.append( (polygon[id_field], candidate_feature[id_field])) else: self.logger.warning( __name__, "(MoreBFS) Intersection between plot (t_id={}) and boundary (t_id={}) is a geometry of type: {}" .format(polygon[id_field], candidate_feature[id_field], intersection_type)) intersection_type = QgsGeometry( multi_inner_rings).intersection( candidate_geometry).type() if intersection_type == QgsWkbTypes.LineGeometry: intersect_less_pairs.append( (polygon[id_field], candidate_feature[id_field])) else: self.logger.warning( __name__, "(Less) Intersection between plot (t_id={}) and boundary (t_id={}) is a geometry of type: {}" .format(polygon[id_field], candidate_feature[id_field], intersection_type)) else: boundary = None if is_multipart and multi_polygon: boundary = multi_polygon.boundary() elif not is_multipart and single_polygon: boundary = single_polygon.boundary() intersection_type = QgsGeometry(boundary).intersection( candidate_geometry).type() if boundary and intersection_type == QgsWkbTypes.LineGeometry: intersect_more_pairs.append( (polygon[id_field], candidate_feature[id_field])) else: self.logger.warning( __name__, "(MoreBFS) Intersection between plot (t_id={}) and boundary (t_id={}) is a geometry of type: {}" .format(polygon[id_field], candidate_feature[id_field], intersection_type)) # free up memory del candidate_features del dict_features gc.collect() return (intersect_more_pairs, intersect_less_pairs) def get_pair_boundary_boundary_point(self, boundary_layer, boundary_point_layer, id_field, use_selection=True): id_field_idx = boundary_layer.fields().indexFromName(id_field) request = QgsFeatureRequest().setSubsetOfAttributes([id_field_idx]) lines = boundary_layer.getSelectedFeatures( request) if use_selection else boundary_layer.getFeatures(request) intersect_pairs = list() if boundary_point_layer.featureCount() == 0: return intersect_pairs id_field_idx = boundary_point_layer.fields().indexFromName(id_field) request = QgsFeatureRequest().setSubsetOfAttributes([id_field_idx]) dict_features = { feature.id(): feature for feature in boundary_point_layer.getFeatures(request) } index = QgsSpatialIndex(boundary_point_layer) candidate_features = None for line in lines: bbox = line.geometry().boundingBox() bbox.scale(1.001) candidates_ids = index.intersects(bbox) candidate_features = [ dict_features[candidate_id] for candidate_id in candidates_ids ] for candidate_feature in candidate_features: #if line.geometry().intersects(candidate_feature.geometry()): # intersect_pair.append(line['t_id'], candidate_feature['t_id']) candidate_point = candidate_feature.geometry().asPoint() for line_vertex in line.geometry().asPolyline(): if line_vertex.x() == candidate_point.x( ) and line_vertex.y() == candidate_point.y(): pair = (line[id_field], candidate_feature[id_field]) if pair not in intersect_pairs: intersect_pairs.append(pair) # free up memory del candidate_features del dict_features gc.collect() return intersect_pairs def get_polyline_as_single_segments(self, polyline): """ Copied from: https://github.com/qgis/QGIS/blob/55203a0fc2b8e35fa2909da77a84bbfde8fcba5c/python/plugins/processing/algs/qgis/Explode.py#L99 """ segments = [] for i in range(polyline.numPoints() - 1): ptA = polyline.pointN(i) ptB = polyline.pointN(i + 1) segment = QgsGeometry(QgsLineString([ptA, ptB])) segments.append(segment) return segments def extract_as_single_segments(self, geom): """ Copied from: https://github.com/qgis/QGIS/blob/55203a0fc2b8e35fa2909da77a84bbfde8fcba5c/python/plugins/processing/algs/qgis/Explode.py#L89 """ segments = [] if geom.isMultipart(): for part in range(geom.constGet().numGeometries()): segments.extend( self.get_polyline_as_single_segments( geom.constGet().geometryN(part))) else: segments.extend( self.get_polyline_as_single_segments(geom.constGet())) return segments def get_overlapping_points(self, point_layer): """ Returns a list of lists, where inner lists are ids of overlapping points, e.g., [[1, 3], [19, 2, 8]]. """ res = list() if point_layer.featureCount() == 0: return res set_points = set() index = QgsSpatialIndex(point_layer) request = QgsFeatureRequest().setSubsetOfAttributes([]) for feature in point_layer.getFeatures(request): if not feature.id() in set_points: ids = index.intersects(feature.geometry().boundingBox()) if len(ids) > 1: # Points do overlap! set_points = set_points.union(set(ids)) res.append(ids) return res def get_overlapping_lines(self, line_layer, use_selection=True): """ Returns a dict whose key is a pair of line ids where there are intersections, and whose value is a list of intersection geometries """ if line_layer.featureCount() == 0: return None feedback = QgsProcessingFeedback() dict_res = processing.run( "model:Overlapping_Boundaries", { 'Boundary': line_layer, 'native:saveselectedfeatures_2:Intersected_Lines': 'memory:', 'native:saveselectedfeatures_3:Intersected_Points': 'memory:' }, feedback=feedback) return dict_res def get_overlapping_polygons(self, polygon_layer): """ Obtains overlapping polygons from a single layer :param polygon_layer: vector layer with geometry type polygon :return: List of lists with pairs of overlapping polygons' ids, e.g., [[1, 2], [1, 3]] """ list_overlapping_polygons = list() if type(polygon_layer) != QgsVectorLayer or \ QgsWkbTypes.PolygonGeometry != polygon_layer.geometryType() or \ polygon_layer.featureCount() == 0: return list_overlapping_polygons request = QgsFeatureRequest().setSubsetOfAttributes([]) dict_features = { feature.id(): feature for feature in polygon_layer.getFeatures(request) } index = QgsSpatialIndex(polygon_layer) candidate_features = None for feature in polygon_layer.getFeatures(request): bbox = feature.geometry().boundingBox() bbox.scale(1.001) candidates_ids = index.intersects(bbox) candidate_features = [ dict_features[candidate_id] for candidate_id in candidates_ids ] for candidate_feature in candidate_features: is_overlap = feature.geometry().overlaps(candidate_feature.geometry()) or \ feature.geometry().contains(candidate_feature.geometry()) or \ feature.geometry().within(candidate_feature.geometry()) if is_overlap: if feature.id() != candidate_feature.id(): overlapping_polygons = sorted( [feature.id(), candidate_feature.id()]) if overlapping_polygons not in list_overlapping_polygons: list_overlapping_polygons.append( overlapping_polygons) # free up memory del candidate_features del dict_features gc.collect() return list_overlapping_polygons def get_intersection_polygons(self, polygon_layer, polygon_id, overlapping_id): feature_polygon = polygon_layer.getFeature(polygon_id) feature_overlap = polygon_layer.getFeature(overlapping_id) listGeoms = list() intersection = feature_polygon.geometry().intersection( feature_overlap.geometry()) if intersection.type() == QgsWkbTypes.PolygonGeometry: listGeoms.append(intersection) elif intersection.wkbType() in [ QgsWkbTypes.GeometryCollection, QgsWkbTypes.GeometryCollectionM, QgsWkbTypes.GeometryCollectionZ, QgsWkbTypes.GeometryCollectionZM ]: for part in intersection.asGeometryCollection(): if part.type() == QgsWkbTypes.PolygonGeometry: listGeoms.append(part) return QgsGeometry.collectGeometry( listGeoms) if len(listGeoms) > 0 else None def get_inner_intersections_between_polygons(self, polygon_layer_1, polygon_layer_2): """ Discard intersections other than inner intersections (i.e., only returns polygon intersections) """ ids = list() list_overlapping = list() request = QgsFeatureRequest().setSubsetOfAttributes([]) dict_features = { feature.id(): feature for feature in polygon_layer_2.getFeatures(request) } index = QgsSpatialIndex(polygon_layer_2) candidate_features = None for feature in polygon_layer_1.getFeatures(request): bbox = feature.geometry().boundingBox() candidates_ids = index.intersects(bbox) candidate_features = [ dict_features[candidate_id] for candidate_id in candidates_ids ] for candidate_feature in candidate_features: candidate_feature_geo = candidate_feature.geometry() if feature.geometry().intersects( candidate_feature_geo ) and not feature.geometry().touches(candidate_feature_geo): intersection = feature.geometry().intersection( candidate_feature_geo) if intersection.type() == QgsWkbTypes.PolygonGeometry: ids.append([feature.id(), candidate_feature.id()]) list_overlapping.append(intersection) elif intersection.wkbType() in [ QgsWkbTypes.GeometryCollection, QgsWkbTypes.GeometryCollectionM, QgsWkbTypes.GeometryCollectionZ, QgsWkbTypes.GeometryCollectionZM ]: for part in intersection.asGeometryCollection(): if part.type() == QgsWkbTypes.PolygonGeometry: ids.append( [feature.id(), candidate_feature.id()]) list_overlapping.append(part) # free up memory del candidate_features del dict_features gc.collect() return ids, QgsGeometry.collectGeometry( list_overlapping) if len(list_overlapping) > 0 else None def get_gaps_in_polygon_layer(self, layer, include_roads): """ Find gaps in a continuous layer in space. Ported/adapted to Python from: https://github.com/qgis/QGIS/blob/2c536307476e205b83d86863b903d7ea9d628f0d/src/plugins/topology/topolTest.cpp#L579-L726 """ request = QgsFeatureRequest().setSubsetOfAttributes([]) features = layer.getFeatures(request) featureCollection = list() for feature in features: if feature.geometry().isEmpty(): continue if not feature.geometry().isGeosValid(): continue if feature.geometry().isMultipart() and feature.geometry().type( ) == QgsWkbTypes.PolygonGeometry: for polygon in feature.geometry().asMultiPolygon(): featureCollection.append( QgsGeometry.fromPolygonXY(polygon)) continue featureCollection.append(feature.geometry()) union_geom = QgsGeometry.unaryUnion(featureCollection) aux_convex_hull = union_geom.convexHull() buffer_extent = QgsGeometry.fromRect(union_geom.boundingBox()).buffer( 2, 3) buffer_diff = buffer_extent.difference( QgsGeometry.fromRect(union_geom.boundingBox())) diff_geoms = buffer_extent.difference(union_geom).difference( buffer_diff) if not diff_geoms: return None feature_error = list() if not diff_geoms.isMultipart(): if include_roads and diff_geoms.touches( union_geom) and diff_geoms.intersects(buffer_diff): print("Unique value and no error") return None for geometry in diff_geoms.asMultiPolygon(): conflict_geom = QgsGeometry.fromPolygonXY(geometry) if not include_roads and conflict_geom.touches( union_geom) and conflict_geom.intersects(buffer_diff): continue if not union_geom.isMultipart() and conflict_geom.touches( union_geom) and conflict_geom.intersects(buffer_diff): continue feature_error.append(conflict_geom) unified_error = QgsGeometry.collectGeometry(feature_error) feature_error.clear() clean_errors = unified_error.intersection(aux_convex_hull) return self.extract_geoms_by_type(clean_errors, [QgsWkbTypes.PolygonGeometry]) def add_topological_vertices(self, layer1, layer2, id_field): """ Modify layer1 adding vertices that are in layer2 and not in layer1 Ideally, we could pass the whole layer2 as parameter for addTopologicalPoints or, at least, pass one multi-geometry containing all geometries from layer2. However, onthe one side, the addTopologicalPoints function doesn't support a layer as parameter and, on the other side, there is a bug in the function that doesn't let it work with multi-geometries. That's why we need to traverse the whole layer2 in search for its individual geometries. We do use a SpatialIndex nonetheless, to improve efficiency. """ if QgsWkbTypes.isMultiType(layer2.wkbType()): layer2 = processing.run("native:multiparttosingleparts", { 'INPUT': layer2, 'OUTPUT': 'memory:' })['OUTPUT'] if layer2.geometryType() == QgsWkbTypes.PolygonGeometry: layer2 = processing.run("ladm_col:polygonstolines", { 'INPUT': layer2, 'OUTPUT': 'memory:' })['OUTPUT'] geom_added = list() index = QgsSpatialIndex(layer2) dict_features_layer2 = None candidate_features = None id_field_idx1 = layer1.fields().indexFromName(id_field) request1 = QgsFeatureRequest().setSubsetOfAttributes([id_field_idx1]) id_field_idx2 = layer2.fields().indexFromName(id_field) request2 = QgsFeatureRequest().setSubsetOfAttributes([id_field_idx2]) with edit(layer1): edit_layer = QgsVectorLayerEditUtils(layer1) dict_features_layer2 = { feature.id(): feature for feature in layer2.getFeatures(request2) } for feature in layer1.getFeatures(request1): bbox = feature.geometry().boundingBox() candidate_ids = index.intersects(bbox) candidate_features = [ dict_features_layer2[candidate_id] for candidate_id in candidate_ids ] for candidate_feature in candidate_features: if candidate_feature.id() not in geom_added: edit_layer.addTopologicalPoints( candidate_feature.geometry()) geom_added.append(candidate_feature.id()) # free up memory del candidate_features del dict_features_layer2 gc.collect() def difference_plot_boundary(self, names, plots_as_lines_layer, boundary_layer, id_field): """ Advanced difference function that, unlike the traditional function, takes into account not shared vertices to build difference geometries. """ approx_diff_layer = processing.run( "native:difference", { 'INPUT': plots_as_lines_layer, 'OVERLAY': boundary_layer, 'OUTPUT': 'memory:' })['OUTPUT'] self.add_topological_vertices(approx_diff_layer, boundary_layer, names.T_ID_F) diff_layer = processing.run( "native:difference", { 'INPUT': approx_diff_layer, 'OVERLAY': boundary_layer, 'OUTPUT': 'memory:' })['OUTPUT'] difference_features = [{ 'geometry': feature.geometry(), 'id': feature[id_field] } for feature in diff_layer.getFeatures()] return difference_features def difference_boundary_plot(self, names, boundary_layer, plot_as_lines_layer, id_field): """ Advanced difference function that, unlike the traditional function, takes into account not shared vertices to build difference geometries. """ approx_diff_layer = processing.run( "native:difference", { 'INPUT': boundary_layer, 'OVERLAY': plot_as_lines_layer, 'OUTPUT': 'memory:' })['OUTPUT'] self.add_topological_vertices(plot_as_lines_layer, approx_diff_layer, names.T_ID_F) diff_layer = processing.run( "native:difference", { 'INPUT': approx_diff_layer, 'OVERLAY': plot_as_lines_layer, 'OUTPUT': 'memory:' })['OUTPUT'] difference_features = [{ 'geometry': feature.geometry(), 'id': feature[id_field] } for feature in diff_layer.getFeatures()] return difference_features def clone_layer(self, layer): layer.selectAll() clone_layer = processing.run("native:saveselectedfeatures", { 'INPUT': layer, 'OUTPUT': 'memory:' })['OUTPUT'] layer.removeSelection() return clone_layer if type(clone_layer) == QgsVectorLayer else False def extract_geoms_by_type(self, geometry_collection, geometry_types): """ Get a list of geometries with type in geometry_types from a geometry collection """ geom_list = list() for geometry in geometry_collection.asGeometryCollection(): if geometry.isMultipart(): for i in range(geometry.numGeometries()): geom_list.append(geometry.geometryN(i)) else: geom_list.append(geometry) return [geom for geom in geom_list if geom.type() in geometry_types] def get_multipart_geoms(self, layer): """ Get a list of geometries and ids with geometry type multipart and multiple geometries """ request = QgsFeatureRequest().setSubsetOfAttributes([]) features = layer.getFeatures(request) featureCollection = list() ids = list() for feature in features: geometry = feature.geometry() const_geom = geometry.constGet() if geometry.isMultipart() and const_geom.partCount() > 1: for i in range(const_geom.numGeometries()): geom = QgsGeometry.fromWkt(const_geom.geometryN(i).asWkt()) featureCollection.append(geom) ids.append(feature.id()) return featureCollection, ids def get_begin_end_vertices_from_lines(self, layer): point_layer = processing.run("qgis:extractspecificvertices", { 'VERTICES': '0,-1', 'INPUT': layer, 'OUTPUT': 'memory:' })['OUTPUT'] point_layer_uniques = processing.run("qgis:deleteduplicategeometries", { 'INPUT': point_layer, 'OUTPUT': 'memory:' })['OUTPUT'] return point_layer_uniques def get_boundaries_connected_to_single_boundary(self, names, boundary_layer): """ Get all boundary lines that have an end vertex with no change in boundary (colindancia), that is boundary lines that are connected with just one boundary line. """ points_layer = self.get_begin_end_vertices_from_lines(boundary_layer) id_field_idx = boundary_layer.fields().indexFromName(names.T_ID_F) request = QgsFeatureRequest().setSubsetOfAttributes([id_field_idx]) dict_features = { feature.id(): feature for feature in boundary_layer.getFeatures(request) } index = QgsSpatialIndex(boundary_layer) ids_boundaries_list = list() candidate_features = None for feature in points_layer.getFeatures(request): bbox = feature.geometry().boundingBox() candidate_ids = index.intersects(bbox) candidate_features = [ dict_features[candidate_id] for candidate_id in candidate_ids ] intersect_ids = list() for candidate_feature in candidate_features: if candidate_feature.geometry().intersects(feature.geometry()): intersect_ids.append(candidate_feature.id()) if len(intersect_ids) == 2: # For valid lines, we get more than two intersections (think # about a 'Y') ids_boundaries_list.extend(intersect_ids) selected_ids = list(set(ids_boundaries_list)) # get unique ids selected_features = [ dict_features[selected_id] for selected_id in selected_ids ] # free up memory del candidate_features del dict_features gc.collect() return selected_features def join_boundary_points_with_boundary_discard_nonmatching( self, boundary_point_layer, boundary_layer, id_field): spatial_join_layer = processing.run( "qgis:joinattributesbylocation", { 'INPUT': boundary_point_layer, 'JOIN': boundary_layer, 'PREDICATE': [0], 'JOIN_FIELDS': [id_field], 'METHOD': 0, 'DISCARD_NONMATCHING': True, 'PREFIX': '', 'OUTPUT': 'memory:' })['OUTPUT'] id_field_idx = spatial_join_layer.fields().indexFromName(id_field) request = QgsFeatureRequest().setSubsetOfAttributes([id_field_idx]) return spatial_join_layer.getFeatures(request) def get_inner_rings_layer(self, names, plot_layer, id_field, use_selection=False): id_field_idx = plot_layer.fields().indexFromName(id_field) request = QgsFeatureRequest().setSubsetOfAttributes([id_field_idx]) polygons = plot_layer.getSelectedFeatures( request) if use_selection else plot_layer.getFeatures(request) layer = QgsVectorLayer( "LineString?crs={}".format(plot_layer.sourceCrs().authid()), "rings", "memory") data_provider = layer.dataProvider() data_provider.addAttributes([QgsField(names.T_ID_F, QVariant.Int)]) layer.updateFields() features = [] for polygon in polygons: polygon_geom = polygon.geometry() is_multipart = polygon_geom.isMultipart() # Does the current multipolygon have inner rings? has_inner_rings = False multi_polygon = None single_polygon = None if is_multipart: multi_polygon = polygon_geom.constGet() # TODO: remove when the error is resolved if type(multi_polygon) != QgsMultiPolygon: geom = QgsMultiPolygon() geom.fromWkt(polygon_geom.asWkt()) multi_polygon = geom for part in range(multi_polygon.numGeometries()): if multi_polygon.ringCount(part) > 1: has_inner_rings = True break else: single_polygon = polygon_geom.constGet() # TODO: remove when the error is resolved if type(single_polygon) != QgsPolygon: geom = QgsPolygon() geom.fromWkt(polygon_geom.asWkt()) single_polygon = geom if single_polygon.numInteriorRings() > 0: has_inner_rings = True if has_inner_rings: if is_multipart and multi_polygon: for i in range(multi_polygon.numGeometries()): temp_polygon = multi_polygon.geometryN(i) # TODO: remove when the error is resolved if type(temp_polygon) != QgsPolygon: geom = QgsPolygon() geom.fromWkt(temp_polygon.asWkt()) temp_polygon = geom for j in range(temp_polygon.numInteriorRings()): new_feature = QgsVectorLayerUtils().createFeature( layer, QgsGeometry( temp_polygon.interiorRing(j).clone()), {0: polygon[id_field]}) features.append(new_feature) elif not is_multipart and single_polygon: for j in range(single_polygon.numInteriorRings()): new_feature = QgsVectorLayerUtils().createFeature( layer, QgsGeometry( single_polygon.interiorRing(j).clone()), {0: polygon[id_field]}) features.append(new_feature) layer.dataProvider().addFeatures(features) layer.updateExtents() layer.reload() return layer def get_connected_segments(self, segment, direction, index, dict_features, items=list(), count_d=0): vertex = None geom = segment.geometry() if direction == 1: vertex = QgsGeometry(geom.vertexAt(0)) elif direction == -1: vertex = QgsGeometry(geom.vertexAt(len(geom.asPolyline()) - 1)) geom = segment.geometry() bbox = vertex.boundingBox() candidates_ids = index.intersects(bbox) candidate_features = [ dict_features[candidate_id] for candidate_id in candidates_ids ] touches = list() for candidate_feature in candidate_features: if candidate_feature.id() != segment.id(): if candidate_feature.geometry().touches(vertex): touches.append(candidate_feature) if len(touches) == 1: if touches[0].id() not in items: items.append(touches[0].id()) return self.get_connected_segments(touches[0], direction, index, dict_features, items, count_d) else: if count_d <= 1: # the direction is changed due to the direction of digitization direction *= -1 # in circular geometries it can happen that the condition of exit is not satisfied, reason for # which the number of consecutive iterations is counted not to stay in an infinite cycle. count_d += 1 return self.get_connected_segments(touches[0], direction, index, dict_features, items, count_d) else: return items else: return items def get_boundary_to_build(self, segment, index, dict_features): id = segment.id() segments_connected = list() way = 1 start_sc = self.get_connected_segments(segment, way, index, dict_features, items=list()) segments_connected.extend(start_sc) way = -1 end_sc = self.get_connected_segments(segment, way, index, dict_features, items=list()) segments_connected.extend(end_sc) if id not in segments_connected: segments_connected.append(id) # segments_connected.sort() # sort items segments_of_the_boundary = list(set(segments_connected)) return segments_of_the_boundary def get_connected_segments_by_selection(self, segment, direction, index, dict_features, items=list(), count_d=0, vertex=None): geom = segment.geometry() if vertex is None: if direction == 1: vertex = QgsGeometry(geom.vertexAt(0)) elif direction == -1: vertex = QgsGeometry(geom.vertexAt(len(geom.asPolyline()) - 1)) bbox = vertex.boundingBox() candidates_ids = index.intersects(bbox) candidate_features = [ dict_features[candidate_id] for candidate_id in candidates_ids ] touches = list() for candidate_feature in candidate_features: if candidate_feature.id() != segment.id(): if candidate_feature.geometry().touches(vertex): touches.append(candidate_feature) if len(touches) == 1: # select next vertex next_geom = touches[0].geometry() start_vertex = QgsGeometry(next_geom.vertexAt(0)) end_vertex = QgsGeometry( next_geom.vertexAt(len(next_geom.asPolyline()) - 1)) next_vertex = None if vertex.asWkt() == start_vertex.asWkt(): next_vertex = end_vertex else: next_vertex = start_vertex if touches[0].id() not in items: items.append(touches[0].id()) return self.get_connected_segments_by_selection( touches[0], direction, index, dict_features, items, count_d, next_vertex) else: if count_d < 1: # in circular geometries it can happen that the condition of exit is not satisfied, # reason for which the number of consecutive iterations is counted not to stay in an infinite cycle. count_d += 1 return self.get_connected_segments_by_selection( touches[0], direction, index, dict_features, items, count_d, next_vertex) else: return items else: return items def get_boundary_to_build_by_selection(self, segment, index, dict_features): id = segment.id() segments_connected = list() direction = 1 start_sc = self.get_connected_segments_by_selection(segment, direction, index, dict_features, items=list(), vertex=None) segments_connected.extend(start_sc) direction = -1 end_sc = self.get_connected_segments_by_selection(segment, direction, index, dict_features, items=list(), vertex=None) segments_connected.extend(end_sc) if id not in segments_connected: segments_connected.append(id) segments_of_the_boundary = list(set(segments_connected)) segments_of_the_boundary.sort() return segments_of_the_boundary def merge_geometries(self, features): geoms = QgsGeometry.fromWkt('GEOMETRYCOLLECTION()') for feature in features: geoms = geoms.combine(feature.geometry()) return geoms def fix_selected_boundaries(self, names, boundary_layer, id_field, selected_ids=list()): selected_features = list() if len(selected_ids) == 0: selected_features = [ feature for feature in boundary_layer.selectedFeatures() ] else: boundary_layer.selectByIds(selected_ids) selected_features = [ feature for feature in boundary_layer.selectedFeatures() ] tmp_segments_layer = processing.run("native:explodelines", { 'INPUT': boundary_layer, 'OUTPUT': 'memory:' })['OUTPUT'] # remove duplicate segments (algorithm don't work with duplicate geometries) segments_layer = processing.run("qgis:deleteduplicategeometries", { 'INPUT': tmp_segments_layer, 'OUTPUT': 'memory:' })['OUTPUT'] id_field_idx = segments_layer.fields().indexFromName(id_field) request = QgsFeatureRequest().setSubsetOfAttributes([id_field_idx]) dict_segments = { feature.id(): feature for feature in segments_layer.getFeatures(request) } index = QgsSpatialIndex(segments_layer) # create relation between feature and yours segments boundary_segments = dict() for feature in boundary_layer.getFeatures(): exp = '"{id_field}" = {id_field_value}'.format( id_field=names.T_ID_F, id_field_value=feature[names.T_ID_F]) segment_ids = [f.id() for f in segments_layer.getFeatures(exp)] segment_ids.sort() if segment_ids: boundary_segments[feature.id()] = segment_ids process_sc = list() total_sc = list() for feature in selected_features: segment_sf_ids = boundary_segments[feature.id()] for segment_sf_id in segment_sf_ids: if segment_sf_id not in total_sc: segment_sf = dict_segments[segment_sf_id] segments_connected = self.get_boundary_to_build_by_selection( segment_sf, index, dict_segments) total_sc.extend(segments_connected) process_sc.append(segments_connected) # It isn't necessary fix the boundaries that are okay. for sc_check in process_sc.copy(): sc_check.sort() for boundary_segment_id in boundary_segments: if sc_check == boundary_segments[boundary_segment_id]: process_sc.remove(sc_check) boundaries_to_del_ids = list() candidate_segments = list() for segments_connected in process_sc: for boundary_id in boundary_segments: if boundary_id not in boundaries_to_del_ids: if len( set(boundary_segments[boundary_id]).intersection( set(segments_connected))) > 0: boundaries_to_del_ids.append(boundary_id) candidate_segments.extend( boundary_segments[boundary_id]) segments_to_include = list(set(candidate_segments) - set(total_sc)) new_geometries = list() # new boundaries result of merge segments for sc_ids in process_sc: selected_features = [dict_segments[sc_id] for sc_id in sc_ids] merge_geom = self.merge_geometries(selected_features) new_geometries.append(merge_geom) # segments to include due to division of lines in segments for segment_id in segments_to_include: segment_geom = dict_segments[segment_id].geometry() new_geometries.append(segment_geom) boundaries_to_del_unique_ids = list(set(boundaries_to_del_ids)) return new_geometries, boundaries_to_del_unique_ids def fix_boundaries(self, layer, id_field): tmp_segments_layer = processing.run("native:explodelines", { 'INPUT': layer, 'OUTPUT': 'memory:' })['OUTPUT'] # remove duplicate segments (algorithm don't with duplicate geometries) segments_layer = processing.run("qgis:deleteduplicategeometries", { 'INPUT': tmp_segments_layer, 'OUTPUT': 'memory:' })['OUTPUT'] id_field_idx = segments_layer.fields().indexFromName(id_field) request = QgsFeatureRequest().setSubsetOfAttributes([id_field_idx]) dict_features = { feature.id(): feature for feature in segments_layer.getFeatures(request) } index = QgsSpatialIndex(segments_layer) process_sc = list() total_sc = list() boundaries_to_del_ids = list() for id in dict_features: if id not in total_sc: segment = dict_features[id] try: segments_connected = self.get_boundary_to_build( segment, index, dict_features) total_sc.extend(segments_connected) process_sc.append(segments_connected) except RecursionError as re: print('Error: {}'.format(re.args[0])) merge_geometries = list() for sc_ids in process_sc: selected_features = [dict_features[sc_id] for sc_id in sc_ids] merge_geom = self.merge_geometries(selected_features) merge_geometries.append(merge_geom) boundaries_to_del = [f.id() for f in layer.getFeatures()] return merge_geometries, boundaries_to_del def get_buildings_out_of_plots(self, building_layer, plot_layer, id_field): building_within_plots = processing.run( "qgis:joinattributesbylocation", { 'INPUT': building_layer, 'JOIN': plot_layer, 'PREDICATE': [5], # within 'JOIN_FIELDS': [id_field], 'METHOD': 0, # 1:m 'DISCARD_NONMATCHING': False, 'PREFIX': '', 'OUTPUT': 'memory:' })['OUTPUT'] # Get buildings that are not cointained in a single plot # This give us buildings that intersect with 0 OR more than one plots building_within_plots.selectByExpression( '"{}_2" IS NOT NULL'.format(id_field)) building_within_plots.dataProvider().deleteFeatures( building_within_plots.selectedFeatureIds()) # Now we run an intersection to classify the subset of buildings in two groups building_plots_with_errors = processing.run( "qgis:joinattributesbylocation", { 'INPUT': building_within_plots, 'JOIN': plot_layer, 'PREDICATE': [0], # intersects 'JOIN_FIELDS': [id_field], 'METHOD': 1, # 1:1 We just want to know whether building intersects plots or not 'DISCARD_NONMATCHING': False, 'PREFIX': '', 'OUTPUT': 'memory:' })['OUTPUT'] buildings_with_no_plot = list() buildings_not_within_a_single_plot = list() for feature in building_plots_with_errors.getFeatures(): if feature['{}_3'.format(id_field)]: buildings_not_within_a_single_plot.append(feature) else: buildings_with_no_plot.append(feature) return buildings_with_no_plot, buildings_not_within_a_single_plot
class ReportGenerator(QObject): LOG_TAB = 'LADM-COL Reports' enable_action_requested = pyqtSignal(str, bool) def __init__(self, ladm_data): QObject.__init__(self) self.ladm_data = ladm_data self.logger = Logger() self.app = AppInterface() self.java_dependency = JavaDependency() self.java_dependency.download_dependency_completed.connect( self.download_java_complete) self.report_dependency = ReportDependency() self.report_dependency.download_dependency_completed.connect( self.download_report_complete) self.encoding = locale.getlocale()[1] # This might be unset if not self.encoding: self.encoding = 'UTF8' self._downloading = False def stderr_ready(self, proc): text = bytes(proc.readAllStandardError()).decode(self.encoding) self.logger.critical(__name__, text, tab=self.LOG_TAB) def stdout_ready(self, proc): text = bytes(proc.readAllStandardOutput()).decode(self.encoding) self.logger.info(__name__, text, tab=self.LOG_TAB) def update_yaml_config(self, db, config_path): text = '' qgs_uri = QgsDataSourceUri(db.uri) with open(os.path.join(config_path, 'config_template.yaml')) as f: text = f.read() text = text.format('{}', DB_USER=qgs_uri.username(), DB_PASSWORD=qgs_uri.password(), DB_HOST=qgs_uri.host(), DB_PORT=qgs_uri.port(), DB_NAME=qgs_uri.database()) new_file_path = os.path.join( config_path, self.get_tmp_filename('yaml_config', 'yaml')) with open(new_file_path, 'w') as new_yaml: new_yaml.write(text) return new_file_path def get_layer_geojson(self, db, layer_name, plot_id, report_type): if report_type == ANNEX_17_REPORT: if layer_name == 'terreno': return db.get_annex17_plot_data(plot_id, 'only_id') elif layer_name == 'terrenos': return db.get_annex17_plot_data(plot_id, 'all_but_id') elif layer_name == 'terrenos_all': return db.get_annex17_plot_data(plot_id, 'all') elif layer_name == 'construcciones': return db.get_annex17_building_data() else: return db.get_annex17_point_data(plot_id) else: #report_type == ANT_MAP_REPORT: if layer_name == 'terreno': return db.get_ant_map_plot_data(plot_id, 'only_id') elif layer_name == 'terrenos': return db.get_ant_map_plot_data(plot_id, 'all_but_id') elif layer_name == 'terrenos_all': return db.get_annex17_plot_data(plot_id, 'all') elif layer_name == 'construcciones': return db.get_annex17_building_data() elif layer_name == 'puntoLindero': return db.get_annex17_point_data(plot_id) else: #layer_name == 'cambio_colindancia': return db.get_ant_map_neighbouring_change_data(plot_id) def update_json_data(self, db, json_spec_file, plot_id, tmp_dir, report_type): json_data = dict() with open(json_spec_file) as f: json_data = json.load(f) json_data['attributes']['id'] = plot_id json_data['attributes']['datasetName'] = db.schema layers = json_data['attributes']['map']['layers'] for layer in layers: layer['geoJson'] = self.get_layer_geojson(db, layer['name'], plot_id, report_type) overview_layers = json_data['attributes']['overviewMap']['layers'] for layer in overview_layers: layer['geoJson'] = self.get_layer_geojson(db, layer['name'], plot_id, report_type) new_json_file_path = os.path.join( tmp_dir, self.get_tmp_filename('json_data_{}'.format(plot_id), 'json')) with open(new_json_file_path, 'w') as new_json: new_json.write(json.dumps(json_data)) return new_json_file_path def get_tmp_dir(self, create_random=True): if create_random: return tempfile.mkdtemp() return tempfile.gettempdir() def get_tmp_filename(self, basename, extension='gpkg'): return "{}_{}.{}".format(basename, str(time.time()).replace(".", ""), extension) def generate_report(self, db, report_type): # Check if mapfish and Jasper are installed, otherwise show where to # download them from and return if not self.report_dependency.check_if_dependency_is_valid(): self.report_dependency.download_dependency(URL_REPORTS_LIBRARIES) return java_home_set = self.java_dependency.set_java_home() if not java_home_set: self.java_dependency.get_java_on_demand() self.logger.info_msg( __name__, QCoreApplication.translate( "ReportGenerator", "Java is a prerequisite. Since it was not found, it is being configured..." )) return plot_layer = self.app.core.get_layer(db, db.names.LC_PLOT_T, load=True) if not plot_layer: return selected_plots = plot_layer.selectedFeatures() if not selected_plots: self.logger.warning_msg( __name__, QCoreApplication.translate( "ReportGenerator", "To generate reports, first select at least a plot!")) return # Where to store the reports? previous_folder = QSettings().value( "Asistente-LADM-COL/reports/save_into_dir", ".") save_into_folder = QFileDialog.getExistingDirectory( None, QCoreApplication.translate( "ReportGenerator", "Select a folder to save the reports to be generated"), previous_folder) if not save_into_folder: self.logger.warning_msg( __name__, QCoreApplication.translate( "ReportGenerator", "You need to select a folder where to save the reports before continuing." )) return QSettings().setValue("Asistente-LADM-COL/reports/save_into_dir", save_into_folder) config_path = os.path.join(DEPENDENCY_REPORTS_DIR_NAME, report_type) json_spec_file = os.path.join(config_path, 'spec_json_file.json') script_name = '' if os.name == 'posix': script_name = 'print' elif os.name == 'nt': script_name = 'print.bat' script_path = os.path.join(DEPENDENCY_REPORTS_DIR_NAME, 'bin', script_name) if not os.path.isfile(script_path): self.logger.warning( __name__, "Script file for reports wasn't found! {}".format(script_path)) return self.enable_action_requested.emit(report_type, False) # Update config file yaml_config_path = self.update_yaml_config(db, config_path) self.logger.debug( __name__, "Config file for reports: {}".format(yaml_config_path)) total = len(selected_plots) step = 0 count = 0 tmp_dir = self.get_tmp_dir() # Progress bar setup progress = QProgressBar() if total == 1: progress.setRange(0, 0) else: progress.setRange(0, 100) progress.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.app.gui.create_progress_message_bar( QCoreApplication.translate("ReportGenerator", "Generating {} report{}...").format( total, '' if total == 1 else 's'), progress) polygons_with_holes = [] multi_polygons = [] for selected_plot in selected_plots: plot_id = selected_plot[db.names.T_ID_F] geometry = selected_plot.geometry() abstract_geometry = geometry.get() if abstract_geometry.ringCount() > 1: polygons_with_holes.append(str(plot_id)) self.logger.warning( __name__, QCoreApplication.translate( "ReportGenerator", "Skipping Annex 17 for plot with {}={} because it has holes. The reporter module does not support such polygons." ).format(db.names.T_ID_F, plot_id)) continue if abstract_geometry.numGeometries() > 1: multi_polygons.append(str(plot_id)) self.logger.warning( __name__, QCoreApplication.translate( "ReportGenerator", "Skipping Annex 17 for plot with {}={} because it is a multi-polygon. The reporter module does not support such polygons." ).format(db.names.T_ID_F, plot_id)) continue # Generate data file json_file = self.update_json_data(db, json_spec_file, plot_id, tmp_dir, report_type) self.logger.debug(__name__, "JSON file for reports: {}".format(json_file)) # Run sh/bat passing config and data files proc = QProcess() proc.readyReadStandardError.connect( functools.partial(self.stderr_ready, proc=proc)) proc.readyReadStandardOutput.connect( functools.partial(self.stdout_ready, proc=proc)) parcel_number = self.ladm_data.get_parcels_related_to_plots( db, [plot_id], db.names.LC_PARCEL_T_PARCEL_NUMBER_F) or [''] file_name = '{}_{}_{}.pdf'.format(report_type, plot_id, parcel_number[0]) current_report_path = os.path.join(save_into_folder, file_name) proc.start(script_path, [ '-config', yaml_config_path, '-spec', json_file, '-output', current_report_path ]) if not proc.waitForStarted(): # Grant execution permissions os.chmod( script_path, stat.S_IXOTH | stat.S_IXGRP | stat.S_IXUSR | stat.S_IRUSR | stat.S_IRGRP) proc.start(script_path, [ '-config', yaml_config_path, '-spec', json_file, '-output', current_report_path ]) if not proc.waitForStarted(): proc = None self.logger.warning( __name__, "Couldn't execute script to generate report...") else: loop = QEventLoop() proc.finished.connect(loop.exit) loop.exec() self.logger.debug(__name__, "{}:{}".format(plot_id, proc.exitCode())) if proc.exitCode() == 0: count += 1 step += 1 progress.setValue(step * 100 / total) os.remove(yaml_config_path) self.enable_action_requested.emit(report_type, True) self.logger.clear_message_bar() if total == count: if total == 1: msg = QCoreApplication.translate( "ReportGenerator", "The report <a href='file:///{}'>{}</a> was successfully generated!" ).format(normalize_local_url(save_into_folder), file_name) else: msg = QCoreApplication.translate( "ReportGenerator", "All reports were successfully generated in folder <a href='file:///{path}'>{path}</a>!" ).format(path=normalize_local_url(save_into_folder)) self.logger.success_msg(__name__, msg) else: details_msg = '' if polygons_with_holes: details_msg += QCoreApplication.translate( "ReportGenerator", " The following polygons were skipped because they have holes and are not supported: {}." ).format(", ".join(polygons_with_holes)) if multi_polygons: details_msg += QCoreApplication.translate( "ReportGenerator", " The following polygons were skipped because they are multi-polygons and are not supported: {}." ).format(", ".join(multi_polygons)) if total == 1: msg = QCoreApplication.translate( "ReportGenerator", "The report for plot {} couldn't be generated!{} See QGIS log (tab '{}') for details." ).format(plot_id, details_msg, self.LOG_TAB) else: if count == 0: msg = QCoreApplication.translate( "ReportGenerator", "No report could be generated!{} See QGIS log (tab '{}') for details." ).format(details_msg, self.LOG_TAB) else: msg = QCoreApplication.translate( "ReportGenerator", "At least one report couldn't be generated!{details_msg} See QGIS log (tab '{log_tab}') for details. Go to <a href='file:///{path}'>{path}</a> to see the reports that were generated." ).format(details_msg=details_msg, path=normalize_local_url(save_into_folder), log_tab=self.LOG_TAB) self.logger.warning_msg(__name__, msg) def download_java_complete(self): if self.java_dependency.fetcher_task and not self.java_dependency.fetcher_task.isCanceled( ): if self.java_dependency.check_if_dependency_is_valid(): self.logger.info_msg( __name__, QCoreApplication.translate( "ReportGenerator", "Java was successfully configured!"), 5) else: self.logger.warning_msg( __name__, QCoreApplication.translate( "ReportGenerator", "You have just canceled the Java dependency download."), 5) def download_report_complete(self): if self.report_dependency.fetcher_task and not self.report_dependency.fetcher_task.isCanceled( ): if self.report_dependency.check_if_dependency_is_valid(): self.logger.info_msg( __name__, QCoreApplication.translate( "ReportGenerator", "Report dependency was successfully configured!"), 5) else: self.logger.warning_msg( __name__, QCoreApplication.translate( "ReportGenerator", "You have just canceled the report dependency download."), 5)
class ManualFeatureCreator(QObject, metaclass=AbstractQObjectMeta): finish_feature_creation = pyqtSignal(str, list) form_rejected = pyqtSignal() exec_form_advanced = pyqtSignal(FeatureFormArgs) form_feature_showing = pyqtSignal(FeatureFormArgs) def __init__(self, iface, layer, feature_name): QObject.__init__(self) self._iface = iface self._app = AppInterface() self._layer = layer self._logger = Logger() self._feature_name = feature_name def create(self): layer = self._get_editing_layer() self.__prepare_layer(layer) self._add_feature(layer) def disconnect_signals(self): try: self._layer.committedFeaturesAdded.disconnect( self.__finish_feature_creation) except: # TODO specify what type of exception is caught pass @abstractmethod def _add_feature(self, layer): pass @abstractmethod def _get_editing_layer(self): pass def __prepare_layer(self, layer): self._iface.layerTreeView().setCurrentLayer(layer) # The original layer. It is not the editing layer self._layer.committedFeaturesAdded.connect( self.__finish_feature_creation) if not layer.isEditable(): layer.startEditing() def _exec_form(self, layer, feature): form_feature_showing_args = FeatureFormArgs(layer, feature) self.form_feature_showing.emit(form_feature_showing_args) dialog = self._iface.getFeatureForm(layer, feature) dialog.rejected.connect(self.form_rejected) dialog.setModal(True) if dialog.exec_(): args = FeatureFormArgs(layer, feature) self.exec_form_advanced.emit(args) saved = layer.commitChanges() if not saved: layer.rollBack() self._logger.warning_msg( __name__, QCoreApplication.translate( "WizardTranslations", "Error while saving changes. {} could not be created." ).format(self._feature_name)) for e in layer.commitErrors(): self._logger.warning(__name__, "Commit error: {}".format(e)) else: layer.rollBack() self._iface.mapCanvas().refresh() dialog.rejected.disconnect(self.form_rejected) def __finish_feature_creation(self, layerId, features): self._layer.committedFeaturesAdded.disconnect( self.__finish_feature_creation) self._logger.info( __name__, "{} committedFeaturesAdded SIGNAL disconnected".format( self._feature_name)) self.finish_feature_creation.emit(layerId, features)
class Role_Registry(metaclass=Singleton): """ Manage all role information. Current role can also be got/set from this class. Roles can set their own GUI configuration even using and overwriting the template gui config. """ COMMON_ACTIONS = [ # Common actions for all roles ACTION_LOAD_LAYERS, ACTION_SCHEMA_IMPORT, ACTION_IMPORT_DATA, ACTION_EXPORT_DATA, ACTION_SETTINGS, ACTION_HELP, ACTION_ABOUT ] def __init__(self): self.logger = Logger() self._registered_roles = dict() self._default_role = BASIC_ROLE role = BASIC_ROLE template_gui = GUI_Config().get_gui_dict(TEMPLATE_GUI) template_gui[TOOLBAR] = [{ # Overwrite list of toolbars WIDGET_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "LADM-COL tools"), OBJECT_NAME: 'ladm_col_toolbar', ACTIONS: [ { # List of toolbars WIDGET_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "Data management"), OBJECT_NAME: 'ladm_col_data_management_toolbar', ICON: DATA_MANAGEMENT_ICON, ACTIONS: [ ACTION_SCHEMA_IMPORT, ACTION_IMPORT_DATA, ACTION_EXPORT_DATA ] }, SEPARATOR, { WIDGET_TYPE: MENU, WIDGET_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "Create Operation objects"), OBJECT_NAME: "ladm_col_operation_toolbar", ICON: OPERATION_ICON, ACTIONS: [ ACTION_CREATE_POINT, ACTION_CREATE_BOUNDARY, SEPARATOR, ACTION_CREATE_PLOT, ACTION_CREATE_BUILDING, ACTION_CREATE_BUILDING_UNIT, ACTION_CREATE_RIGHT_OF_WAY, ACTION_FILL_RIGHT_OF_WAY_RELATIONS, SEPARATOR, ACTION_CREATE_EXT_ADDRESS, SEPARATOR, ACTION_CREATE_PARCEL, SEPARATOR, ACTION_CREATE_PARTY, ACTION_CREATE_GROUP_PARTY, SEPARATOR, ACTION_CREATE_RIGHT, ACTION_CREATE_RESTRICTION, SEPARATOR, ACTION_CREATE_ADMINISTRATIVE_SOURCE, ACTION_CREATE_SPATIAL_SOURCE, ACTION_UPLOAD_PENDING_SOURCE ] }, SEPARATOR, ACTION_FINALIZE_GEOMETRY_CREATION, { WIDGET_TYPE: MENU, WIDGET_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "Structuring tools"), OBJECT_NAME: "ladm_col_structuring_tools_toolbar", ICON: STRUCTURING_TOOLS_ICON, ACTIONS: [ ACTION_BUILD_BOUNDARY, ACTION_MOVE_NODES, ACTION_FILL_BFS, ACTION_FILL_MORE_BFS_AND_LESS ] }, SEPARATOR, ACTION_LOAD_LAYERS, ACTION_PARCEL_QUERY ] }] role_dict = { ROLE_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "Basic"), ROLE_DESCRIPTION: QCoreApplication.translate( "AsistenteLADMCOLPlugin", "The basic role helps you to explore the LADM_COL assistant main functionalities." ), ROLE_ACTIONS: [ ACTION_DOWNLOAD_GUIDE, ACTION_CREATE_POINT, ACTION_CREATE_BOUNDARY, ACTION_CREATE_PLOT, ACTION_CREATE_BUILDING, ACTION_CREATE_BUILDING_UNIT, ACTION_CREATE_RIGHT_OF_WAY, ACTION_CREATE_EXT_ADDRESS, ACTION_CREATE_PARCEL, ACTION_CREATE_RIGHT, ACTION_CREATE_RESTRICTION, ACTION_CREATE_PARTY, ACTION_CREATE_GROUP_PARTY, ACTION_CREATE_ADMINISTRATIVE_SOURCE, ACTION_CREATE_SPATIAL_SOURCE, ACTION_UPLOAD_PENDING_SOURCE, ACTION_IMPORT_FROM_INTERMEDIATE_STRUCTURE, ACTION_BUILD_BOUNDARY, ACTION_MOVE_NODES, ACTION_FINALIZE_GEOMETRY_CREATION, ACTION_FILL_BFS, ACTION_FILL_MORE_BFS_AND_LESS, ACTION_FILL_RIGHT_OF_WAY_RELATIONS, ACTION_PARCEL_QUERY, ACTION_CHECK_QUALITY_RULES ], ROLE_GUI_CONFIG: template_gui } self.register_role(role, role_dict) role = SUPPLIES_PROVIDER_ROLE template_gui = GUI_Config().get_gui_dict(TEMPLATE_GUI) template_gui[TOOLBAR] = [{ # Overwrite list of toolbars WIDGET_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "LADM-COL tools"), OBJECT_NAME: 'ladm_col_toolbar', ACTIONS: [ { # List of toolbars WIDGET_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "Transitional System"), OBJECT_NAME: 'ladm_col_toolbar_st', ICON: ST_ICON, ACTIONS: [ACTION_ST_LOGIN, ACTION_ST_LOGOUT] }, SEPARATOR, ACTION_SCHEMA_IMPORT, ACTION_RUN_ETL_COBOL, ACTION_RUN_ETL_SNC, ACTION_FIND_MISSING_COBOL_SUPPLIES, ACTION_LOAD_LAYERS, ACTION_EXPORT_DATA ] }] role_dict = { ROLE_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "Supplies Provider"), ROLE_DESCRIPTION: QCoreApplication.translate( "AsistenteLADMCOLPlugin", "The Supplies Provider role generates a XTF file with supplies data for the Manager role." ), ROLE_ACTIONS: [ ACTION_RUN_ETL_COBOL, ACTION_RUN_ETL_SNC, ACTION_FIND_MISSING_COBOL_SUPPLIES, ACTION_ST_LOGIN, ACTION_ST_LOGOUT ], ROLE_GUI_CONFIG: template_gui } self.register_role(role, role_dict) role = OPERATOR_ROLE role_dict = { ROLE_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "Operator"), ROLE_DESCRIPTION: QCoreApplication.translate( "AsistenteLADMCOLPlugin", "The operator is in charge of capturing current cadastral data." ), ROLE_ACTIONS: [ ACTION_CREATE_POINT, ACTION_CREATE_BOUNDARY, ACTION_CREATE_PLOT, ACTION_CREATE_BUILDING, ACTION_CREATE_BUILDING_UNIT, ACTION_CREATE_RIGHT_OF_WAY, ACTION_CREATE_EXT_ADDRESS, ACTION_CREATE_PARCEL, ACTION_CREATE_RIGHT, ACTION_CREATE_RESTRICTION, ACTION_CREATE_PARTY, ACTION_CREATE_GROUP_PARTY, ACTION_CREATE_ADMINISTRATIVE_SOURCE, ACTION_CREATE_SPATIAL_SOURCE, ACTION_UPLOAD_PENDING_SOURCE, ACTION_IMPORT_FROM_INTERMEDIATE_STRUCTURE, ACTION_BUILD_BOUNDARY, ACTION_MOVE_NODES, ACTION_FINALIZE_GEOMETRY_CREATION, ACTION_FILL_BFS, ACTION_FILL_MORE_BFS_AND_LESS, ACTION_FILL_RIGHT_OF_WAY_RELATIONS, ACTION_CHANGE_DETECTION_SETTINGS, ACTION_CHANGE_DETECTION_ALL_PARCELS, ACTION_CHANGE_DETECTION_PER_PARCEL, ACTION_ST_LOGIN, ACTION_ST_LOGOUT, ACTION_PARCEL_QUERY, ACTION_CHECK_QUALITY_RULES ], ROLE_GUI_CONFIG: {} # Let the gui builder use the template GUI config. } self.register_role(role, role_dict) role = MANAGER_ROLE template_gui = GUI_Config().get_gui_dict(TEMPLATE_GUI) template_gui[TOOLBAR] = [{ # Overwrite list of toolbars WIDGET_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "LADM-COL tools"), OBJECT_NAME: 'ladm_col_toolbar', ACTIONS: [ { # List of toolbars WIDGET_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "Transitional System"), OBJECT_NAME: 'ladm_col_toolbar_st', ICON: ST_ICON, ACTIONS: [ACTION_ST_LOGIN, ACTION_ST_LOGOUT] }, SEPARATOR, ACTION_LOAD_LAYERS, ACTION_INTEGRATE_SUPPLIES, SEPARATOR, ACTION_CHECK_QUALITY_RULES, ACTION_PARCEL_QUERY, SEPARATOR, { # List of toolbars WIDGET_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "Change Detection"), OBJECT_NAME: 'ladm_col_change_detection_toolbar', ICON: CHANGE_DETECTION_ICON, ACTIONS: [ ACTION_CHANGE_DETECTION_SETTINGS, SEPARATOR, ACTION_CHANGE_DETECTION_PER_PARCEL, ACTION_CHANGE_DETECTION_ALL_PARCELS ] }, SEPARATOR, { # List of toolbars WIDGET_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "Reports"), OBJECT_NAME: 'ladm_col_reports_toolbar', ICON: REPORTS_ICON, ACTIONS: [ACTION_REPORT_ANNEX_17, ACTION_REPORT_ANT] } ] }] role_dict = { ROLE_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "Manager"), ROLE_DESCRIPTION: QCoreApplication.translate( "AsistenteLADMCOLPlugin", "The manager is in charge of preparing supplies for operators as well as validating and managing the data provided by operators." ), ROLE_ACTIONS: [ ACTION_CHANGE_DETECTION_SETTINGS, ACTION_CHANGE_DETECTION_ALL_PARCELS, ACTION_CHANGE_DETECTION_PER_PARCEL, ACTION_ST_LOGIN, ACTION_ST_LOGOUT, ACTION_REPORT_ANNEX_17, ACTION_REPORT_ANT, ACTION_INTEGRATE_SUPPLIES, ACTION_PARCEL_QUERY, ACTION_CHECK_QUALITY_RULES ], ROLE_GUI_CONFIG: template_gui } self.register_role(role, role_dict) role = ADVANCED_ROLE template_gui = GUI_Config().get_gui_dict(TEMPLATE_GUI) template_gui[TOOLBAR] = [{ # List of toolbars WIDGET_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "LADM-COL tools"), OBJECT_NAME: 'ladm_col_toolbar', ACTIONS: [ { # List of toolbars WIDGET_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "Transitional System"), OBJECT_NAME: 'ladm_col_st_toolbar', ICON: ST_ICON, ACTIONS: [ACTION_ST_LOGIN, ACTION_ST_LOGOUT] }, SEPARATOR, { WIDGET_TYPE: MENU, WIDGET_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "Create Operation objects"), OBJECT_NAME: "ladm_col_operation_toolbar", ICON: OPERATION_ICON, ACTIONS: [ ACTION_CREATE_POINT, ACTION_CREATE_BOUNDARY, SEPARATOR, ACTION_CREATE_PLOT, ACTION_CREATE_BUILDING, ACTION_CREATE_BUILDING_UNIT, ACTION_CREATE_RIGHT_OF_WAY, ACTION_FILL_RIGHT_OF_WAY_RELATIONS, SEPARATOR, ACTION_CREATE_EXT_ADDRESS, SEPARATOR, ACTION_CREATE_PARCEL, SEPARATOR, ACTION_CREATE_PARTY, ACTION_CREATE_GROUP_PARTY, SEPARATOR, ACTION_CREATE_RIGHT, ACTION_CREATE_RESTRICTION, SEPARATOR, ACTION_CREATE_ADMINISTRATIVE_SOURCE, ACTION_CREATE_SPATIAL_SOURCE, ACTION_UPLOAD_PENDING_SOURCE ] }, SEPARATOR, ACTION_LOAD_LAYERS, SEPARATOR, ACTION_FINALIZE_GEOMETRY_CREATION, ACTION_BUILD_BOUNDARY, ACTION_MOVE_NODES, SEPARATOR, ACTION_FILL_BFS, ACTION_FILL_MORE_BFS_AND_LESS, SEPARATOR, ACTION_SETTINGS ] }] role_dict = { ROLE_NAME: QCoreApplication.translate("AsistenteLADMCOLPlugin", "Advanced"), ROLE_DESCRIPTION: QCoreApplication.translate( "AsistenteLADMCOLPlugin", "The advanced role has access to all the functionality."), ROLE_ACTIONS: [ALL_ACTIONS], ROLE_GUI_CONFIG: template_gui } self.register_role(role, role_dict) def register_role(self, role_key, role_dict): """ Register roles for the LADM_COL assistant. Roles have access only to certain GUI controls. :param role_key: Role unique identifier :param role_dict: Dictionary with the following information: ROLE_NAME: Name of the role ROLE_DESCRIPTION: Explains what this role is about ROLE_ACTIONS: List of actions a role has access to :return: Whether the role was successfully registered or not. """ valid = False if ROLE_NAME in role_dict and ROLE_DESCRIPTION in role_dict and ROLE_ACTIONS in role_dict and ROLE_GUI_CONFIG in role_dict: self._registered_roles[role_key] = deepcopy(role_dict) valid = True else: self.logger.error( __name__, "Role '{}' is not defined correctly and could not be registered! Check the role_dict parameter." .format(role_key)) return valid def get_active_role(self): return QSettings().value("Asistente-LADM_COL/roles/current_role_key", self._default_role) def active_role_already_set(self): """ Whether we have set an active role already or not. :return: True if the current_role_key variable is stored in QSettings. False otherwise. """ return QSettings().value("Asistente-LADM_COL/roles/current_role_key", False) is not False def set_active_role(self, role_key): res = False if role_key in self._registered_roles: res = True else: self.logger.warning( __name__, "Role '{}' was not found, the default role is now active.". format(role_key)) role_key = self._default_role QSettings().setValue("Asistente-LADM_COL/roles/current_role_key", role_key) self.logger.info(__name__, "Role '{}' is now active!".format(role_key)) return res def set_active_default_role(self): QSettings().setValue("Asistente-LADM_COL/roles/current_role_key", self._default_role) self.logger.info( __name__, "Default role '{}' is now active!".format(self._default_role)) return True def get_roles_info(self): return {k: v[ROLE_NAME] for k, v in self._registered_roles.items()} def get_role_name(self, role_key): if role_key not in self._registered_roles: self.logger.error( __name__, "Role '{}' was not found, returning default role's name". format(role_key)) role_key = self._default_role return self._registered_roles[role_key][ROLE_NAME] def get_role_description(self, role_key): if role_key not in self._registered_roles: self.logger.error( __name__, "Role '{}' was not found, returning default role's decription". format(role_key)) role_key = self._default_role return self._registered_roles[role_key][ROLE_DESCRIPTION] def get_role_actions(self, role_key): if role_key not in self._registered_roles: self.logger.error( __name__, "Role '{}' was not found, returning default role's actions.". format(role_key)) role_key = self._default_role return list( set(self._registered_roles[role_key][ROLE_ACTIONS] + self.COMMON_ACTIONS)) def get_role_gui_config(self, role_key): if role_key not in self._registered_roles: self.logger.error( __name__, "Role '{}' was not found, returning default role's GUI configuration." .format(role_key)) role_key = self._default_role return self._registered_roles[role_key][ROLE_GUI_CONFIG]
class DockWidgetQueries(QgsDockWidget, DOCKWIDGET_UI): def __init__(self, iface, controller, parent=None): super(DockWidgetQueries, self).__init__(None) self.setupUi(self) self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) self.iface = iface self._controller = controller self.logger = Logger() self.app = AppInterface() self.canvas = iface.mapCanvas() self.active_map_tool_before_custom = None self._identify_tool = None self._fill_combos() self.btn_identify_plot.setIcon( QIcon(":/Asistente-LADM-COL/resources/images/spatial_unit.png")) self.tab_results.setTabEnabled( TAB_BASIC_INFO_INDEX, False) # TODO: Remove when queries support LevCat 1.2 self.tab_results.setTabEnabled( TAB_PHYSICAL_INFO_INDEX, False) # TODO: Remove when queries support LevCat 1.2 self.tab_results.setTabEnabled( TAB_ECONOMIC_INFO_INDEX, False) # TODO: Remove when queries support LevCat 1.2 self.tab_results.setCurrentIndex( TAB_LEGAL_INFO_INDEX ) # TODO: Remove when queries support LevCat 1.2 # Set connections self._controller.close_view_requested.connect(self._close_dock_widget) self.btn_alphanumeric_query.clicked.connect(self._alphanumeric_query) self.cbo_parcel_fields.currentIndexChanged.connect( self._search_field_updated) self.btn_identify_plot.clicked.connect(self._btn_plot_toggled) self.btn_query_informality.clicked.connect(self._query_informality) self.btn_next_informal_parcel.clicked.connect( self._query_next_informal_parcel) self.btn_previous_informal_parcel.clicked.connect( self._query_previous_informal_parcel) # Context menu self._set_context_menus() # Create maptool self.maptool_identify = QgsMapToolIdentifyFeature(self.canvas) self._initialize_field_values_line_edit() self._update_informal_controls() def _search_field_updated(self, index=None): self._initialize_field_values_line_edit() def _initialize_field_values_line_edit(self): self.txt_alphanumeric_query.setLayer(self._controller.parcel_layer()) idx = self._controller.parcel_layer().fields().indexOf( self.cbo_parcel_fields.currentData()) self.txt_alphanumeric_query.setAttributeIndex(idx) def _set_context_menus(self): self.tree_view_basic.setContextMenuPolicy(Qt.CustomContextMenu) self.tree_view_basic.customContextMenuRequested.connect( self._show_context_menu) self.tree_view_legal.setContextMenuPolicy(Qt.CustomContextMenu) self.tree_view_legal.customContextMenuRequested.connect( self._show_context_menu) self.tree_view_physical.setContextMenuPolicy(Qt.CustomContextMenu) self.tree_view_physical.customContextMenuRequested.connect( self._show_context_menu) self.tree_view_economic.setContextMenuPolicy(Qt.CustomContextMenu) self.tree_view_economic.customContextMenuRequested.connect( self._show_context_menu) def _close_dock_widget(self): # Deactivate custom tool and close dockwidget self._controller.disconnect_plot_layer() self._controller.disconnect_parcel_layer() self._initialize_tools(new_tool=None, old_tool=self.maptool_identify) self._btn_plot_toggled() self.close( ) # The user needs to use the menus again, which will start everything from scratch def _fill_combos(self): self.cbo_parcel_fields.clear() self.cbo_parcel_fields.addItem( QCoreApplication.translate("DockWidgetQueries", "Parcel Number"), self._controller.parcel_number_name()) self.cbo_parcel_fields.addItem( QCoreApplication.translate("DockWidgetQueries", "Previous Parcel Number"), self._controller.previous_parcel_number_name()) self.cbo_parcel_fields.addItem( QCoreApplication.translate("DockWidgetQueries", "Folio de Matrícula Inmobiliaria"), self._controller.fmi_name()) def _initialize_tools(self, new_tool, old_tool): if self.maptool_identify == old_tool: # custom identify was deactivated try: self.canvas.mapToolSet.disconnect(self._initialize_tools) except TypeError as e: pass self.btn_identify_plot.setChecked(False) else: # custom identify was activated pass def _btn_plot_toggled(self): if self.btn_identify_plot.isChecked(): self._prepare_identify_plot() else: # The button was toggled and deactivated, go back to the previous tool self.canvas.setMapTool(self.active_map_tool_before_custom) def _prepare_identify_plot(self): """ Custom Identify tool was activated, prepare everything for identifying plots """ self.active_map_tool_before_custom = self.canvas.mapTool() self.btn_identify_plot.setChecked(True) self.canvas.mapToolSet.connect(self._initialize_tools) self.maptool_identify.setLayer(self._controller.plot_layer()) cursor = QCursor() cursor.setShape(Qt.PointingHandCursor) self.maptool_identify.setCursor(cursor) self.canvas.setMapTool(self.maptool_identify) try: self.maptool_identify.featureIdentified.disconnect() except TypeError as e: pass self.maptool_identify.featureIdentified.connect( self._search_data_by_plot) def _search_data_by_plot(self, plot_feature): plot_t_id = plot_feature[self._controller.t_id_name()] self.app.gui.flash_features(self._controller.plot_layer(), [plot_feature.id()]) with OverrideCursor(Qt.WaitCursor): if not self.isVisible(): self.show() self._search_data_by_component(plot_t_ids=[plot_t_id], zoom_and_select=False) self._controller.plot_layer().selectByIds([plot_feature.id()]) def _search_data_by_component(self, **kwargs): """ Perform the searches by component and fill tree views :param kwargs: A dict with search criteria. """ self._controller.plot_layer().removeSelection() # Read zoom_and_select parameter and remove it from kwargs bZoom = False if 'zoom_and_select' in kwargs: bZoom = kwargs['zoom_and_select'] del kwargs['zoom_and_select'] if 'parcel_number' in kwargs and kwargs['parcel_number'] == NULL: self.logger.warning( __name__, QCoreApplication.translate( "DockWidgetQueries", "The parcel number is NULL! We cannot retrieve data for parcels with NULL parcel numbers." )) # records = self._controller.search_data_basic_info(**kwargs) # if bZoom: # self._controller.zoom_to_resulting_plots(records) # self._setup_tree_view(self.tree_view_basic, records) records = self._controller.search_data_legal_info(**kwargs) self._setup_tree_view(self.tree_view_legal, records) # records = self._controller.search_data_physical_info(**kwargs) # self._setup_tree_view(self.tree_view_physical, records) # records = self._controller.search_data_economic_info(**kwargs) # self._setup_tree_view(self.tree_view_economic, records) def _setup_tree_view(self, tree_view, records): """ Configure result tree views :param tree_view: Tree view to be updated :param records: List of dicts. A dict per plot: {id: 21, attributes: {...}} """ tree_view.setModel(self._controller.create_model(records)) self._collapse_tree_view_items(tree_view) self._add_thumbnails_to_tree_view(tree_view) def _collapse_tree_view_items(self, tree_view): """ Collapse tree view items based on a property """ tree_view.expandAll() for idx in tree_view.model().getCollapseIndexList(): tree_view.collapse(idx) def _add_thumbnails_to_tree_view(self, tree_view): """ Gets a list of model indexes corresponding to extFiles objects to show a preview """ model = tree_view.model() for idx in model.getPixmapIndexList(): url = model.data(idx, Qt.UserRole)['url'] res, image = self._controller.download_image("{}{}".format( url, SUFFIX_GET_THUMBNAIL)) if res: pixmap = QPixmap() pixmap.loadFromData(image) label = QLabel() label.setPixmap(pixmap) tree_view.setIndexWidget(idx, label) def _alphanumeric_query(self): option = self.cbo_parcel_fields.currentData() query = self.txt_alphanumeric_query.value() if query: if option == self._controller.fmi_name(): self._search_data_by_component(parcel_fmi=query, zoom_and_select=True) elif option == self._controller.parcel_number_name(): self._search_data_by_component(parcel_number=query, zoom_and_select=True) else: # previous_parcel_number self._search_data_by_component(previous_parcel_number=query, zoom_and_select=True) else: self.logger.info_msg( __name__, QCoreApplication.translate("DockWidgetQueries", "First enter a query")) def _show_context_menu(self, point): tree_view = self.sender() index = tree_view.indexAt(point) context_menu = QMenu("Context menu") index_data = index.data(Qt.UserRole) if index_data is None: return if "value" in index_data: action_copy = QAction( QCoreApplication.translate("DockWidgetQueries", "Copy value")) action_copy.triggered.connect( partial(self._controller.copy_value, index_data["value"])) context_menu.addAction(action_copy) context_menu.addSeparator() if "url" in index_data: action_open_url = QAction( QCoreApplication.translate("DockWidgetQueries", "Open URL")) action_open_url.triggered.connect( partial(self._controller.open_url, index_data["url"])) context_menu.addAction(action_open_url) context_menu.addSeparator() # Configure actions for tables/layers if "type" in index_data and "id" in index_data: table_name = index_data["type"] t_id = index_data["id"] if table_name == self._controller.parcel_layer_name(): layer = self._controller.parcel_layer() self.app.core.activate_layer_requested.emit(layer) else: layer = self._controller.get_layer(table_name) if layer is not None: if layer.isSpatial(): action_zoom_to_feature = QAction( QCoreApplication.translate( "DockWidgetQueries", "Zoom to {} with {}={}").format( table_name, self._controller.t_id_name(), t_id)) action_zoom_to_feature.triggered.connect( partial(self._controller.zoom_to_feature, layer, t_id)) context_menu.addAction(action_zoom_to_feature) if table_name == self._controller.parcel_layer_name(): # We show a handy option to zoom to related plots plot_ids = self._controller.get_plots_related_to_parcel( t_id) if plot_ids: action_zoom_to_plots = QAction( QCoreApplication.translate( "DockWidgetQueries", "Zoom to related plot(s)")) action_zoom_to_plots.triggered.connect( partial(self._controller.zoom_to_plots, plot_ids)) context_menu.addAction(action_zoom_to_plots) action_open_feature_form = QAction( QCoreApplication.translate( "DockWidgetQueries", "Open form for {} with {}={}").format( table_name, self._controller.t_id_name(), t_id)) action_open_feature_form.triggered.connect( partial(self._controller.open_feature_form, layer, t_id)) context_menu.addAction(action_open_feature_form) if context_menu.actions(): context_menu.exec_(tree_view.mapToGlobal(point)) def _query_informality(self): first_parcel_number, current, total = self._controller.query_informal_parcels( ) self._search_data_by_component(parcel_number=first_parcel_number, zoom_and_select=True) self._update_informal_controls(first_parcel_number, current, total) if not total: self.logger.info_msg( __name__, QCoreApplication.translate( "DockWidgetQueries", "There are no informal parcels in this database!")) def _update_informal_controls(self, parcel_number='', current=0, total=0): """ Update controls (reset labels, enable buttons if we have informality) """ self._update_informal_labels(parcel_number, current, total) self.btn_query_informality.setText( QCoreApplication.translate("DockWidgetQueries", "Restart" ) if current else QCoreApplication. translate("DockWidgetQueries", "Start")) enable = total > 1 # At least 2 to enable buttons that traverse the parcels self.btn_next_informal_parcel.setEnabled(enable) self.btn_previous_informal_parcel.setEnabled(enable) def _update_informal_labels(self, parcel_number='', current=0, total=0): self.lbl_informal_parcel_number.setText( parcel_number if parcel_number != NULL else 'NULL') out_of = '' if current and total: out_of = QCoreApplication.translate("DockWidgetQueries", "{} out of {}").format( current, total) self.lbl_informal_out_of_total.setText(out_of) def _query_next_informal_parcel(self): parcel_number, current, total = self._controller.get_next_informal_parcel( ) self._search_data_by_component(parcel_number=parcel_number, zoom_and_select=True) self._update_informal_controls(parcel_number, current, total) def _query_previous_informal_parcel(self): parcel_number, current, total = self._controller.get_previous_informal_parcel( ) self._search_data_by_component(parcel_number=parcel_number, zoom_and_select=True) self._update_informal_controls(parcel_number, current, total) def closeEvent(self, event): try: self.canvas.mapToolSet.disconnect(self._initialize_tools) except TypeError as e: pass self.canvas.setMapTool(self.active_map_tool_before_custom)
class SuppliesETLWizard(QWizard, WIZARD_UI): on_result = pyqtSignal( bool) # whether the tool was run successfully or not def __init__(self, db, conn_manager, parent=None): QWizard.__init__(self, parent) self.setupUi(self) self._db = db self.conn_manager = conn_manager self.parent = parent self.logger = Logger() self.app = AppInterface() self.names = self._db.names self.help_strings = HelpStrings() self._data_source_widget = None self.db_source = SUPPLIES_DB_SOURCE self.tool_name = "" self._running_tool = False self._db_was_changed = False # To postpone calling refresh gui until we close this dialog instead of settings self.progress_configuration(0, 1) # start from: 0, number of steps: 1 self.wizardPage2.setButtonText( QWizard.CustomButton1, QCoreApplication.translate("SuppliesETLWizard", "Run ETL")) self.wizardPage1.setButtonText( QWizard.CancelButton, QCoreApplication.translate("SuppliesETLWizard", "Close")) self.wizardPage2.setButtonText( QWizard.CancelButton, QCoreApplication.translate("SuppliesETLWizard", "Close")) # Auxiliary data to set nonlinear next pages self.pages = [self.wizardPage1, self.wizardPage2, self.wizardPage3] self.dict_pages_ids = { self.pages[idx]: pid for idx, pid in enumerate(self.pageIds()) } # Set connections self.rad_snc_data.toggled.connect(self.etl_option_changed) self.etl_option_changed() # Initialize it self.button(QWizard.CustomButton1).clicked.connect( self.import_button_clicked) self.button(QWizard.HelpButton).clicked.connect(self.show_help) self.currentIdChanged.connect(self.current_page_changed) self.finished.connect(self.finished_slot) self.btn_browse_connection.clicked.connect(self.show_settings) # Initialize self.current_page_changed(1) self.update_connection_info() self.restore_settings() self.initialize_feedback() # Set MessageBar for QWizard self.bar = QgsMessageBar() self.bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.setLayout(QGridLayout()) self.layout().addWidget(self.bar, 0, 0, Qt.AlignTop) def current_page_changed(self, id): """ Reset the Next button. Needed because Next might have been disabled by a condition in a another SLOT. """ #enable_next_wizard(self) button_list = [ QWizard.HelpButton, QWizard.Stretch, QWizard.BackButton, QWizard.CustomButton1, QWizard.NextButton, QWizard.FinishButton, QWizard.CancelButton ] not_visible = [] if id == self.dict_pages_ids[self.wizardPage1]: self.setWindowTitle( QCoreApplication.translate("SuppliesETLWizard", "Run supplies ETL")) button_list.remove(QWizard.BackButton) button_list.remove(QWizard.CustomButton1) button_list.remove(QWizard.FinishButton) elif id == self.dict_pages_ids[self.wizardPage2]: button_list.remove(QWizard.FinishButton) not_visible.append(self.NextButton) self.load_data_source_controls() if self.rad_snc_data.isChecked(): self.setWindowTitle( QCoreApplication.translate("SuppliesETLWizard", "ETL: SNC to Supplies model")) else: self.setWindowTitle( QCoreApplication.translate("SuppliesETLWizard", "ETL: Cobol to Supplies model")) elif id == self.dict_pages_ids[self.wizardPage3]: self.bar.clearWidgets() button_list.remove(QWizard.CustomButton1) button_list.remove(QWizard.NextButton) button_list.remove(QWizard.BackButton) self.wizardPage3.setFinalPage(True) self.setButtonLayout(button_list) for button in not_visible: self.button(button).setVisible(False) def etl_option_changed(self): """ Adjust help, names and titles according to the selected option """ if self.rad_snc_data.isChecked(): self.tool_name = QCoreApplication.translate( "SuppliesETLWizard", "ETL-SNC") self.txt_help_page_2.setHtml( self.help_strings.WIZ_SUPPLIES_ETL_PAGE_2.format("del SNC")) elif self.rad_cobol_data.isChecked(): # self.rad_cobol_data is checked self.tool_name = QCoreApplication.translate( "SuppliesETLWizard", "ETL-Cobol") self.txt_help_page_2.setHtml( self.help_strings.WIZ_SUPPLIES_ETL_PAGE_2.format("de Cobol")) def load_data_source_controls(self): self.clear_data_source_widget() if self.rad_snc_data.isChecked(): self._data_source_widget = SNCDataSourceWidget() else: # Cobol self._data_source_widget = CobolDataSourceWidget() self._data_source_widget.input_data_changed.connect( self.set_import_button_enabled) self._data_source_widget.emit_input_data_changed( ) # Initialize input validation self.data_source_layout.addWidget(self._data_source_widget) self._data_source_widget.setVisible(True) def clear_data_source_widget(self): while self.data_source_layout.count(): child = self.data_source_layout.takeAt(0) if child.widget(): child.widget().setVisible(False) def initialize_feedback(self): self.progress.setValue(0) self.progress.setVisible(False) self.custom_feedback = CustomFeedback() self.custom_feedback.progressChanged.connect(self.progress_changed) self.set_gui_controls_enabled(True) def progress_configuration(self, base, num_process): """ :param base: Where to start counting from :param num_process: Number of steps """ self.progress_base = base self.progress_maximum = 100 * num_process self.progress.setMaximum(self.progress_maximum) def progress_changed(self): QCoreApplication.processEvents() # Listen to cancel from the user self.progress.setValue(self.progress_base + self.custom_feedback.progress()) def set_gui_controls_enabled(self, enable): self.gbx_data_source.setEnabled(enable) self.target_data.setEnabled(enable) self.set_import_button_enabled(enable) def set_import_button_enabled(self, enable): self.button(QWizard.CustomButton1).setEnabled(enable) def import_button_clicked(self): self.bar.clearWidgets() self.save_settings() etl_result = False if self.rad_snc_data.isChecked(): etl = ETLSNC(self.names, self._data_source_widget) else: # Cobol etl = ETLCobol(self.names, self._data_source_widget) if self._db.test_connection()[0]: reply = QMessageBox.question( self, QCoreApplication.translate("SuppliesETLWizard", "Warning"), QCoreApplication.translate( "SuppliesETLWizard", "The database <i>{}</i> already has a valid LADM-COL structure.<br/><br/>If such database has any data, loading data into it might cause invalid data.<br/><br/>Do you still want to continue?" ).format(self._db.get_description_conn_string()), QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.Yes: self.set_gui_controls_enabled(False) self.button(self.BackButton).setEnabled(False) self.button(self.CustomButton1).setEnabled(False) self.button(self.CancelButton).setText( QCoreApplication.translate("SuppliesETLWizard", "Cancel")) with OverrideCursor(Qt.WaitCursor): res_alpha, msg_alpha = etl.load_alphanumeric_layers() if res_alpha: res_spatial, msg_spatial = etl.load_spatial_layers() if res_spatial: res_model, msg_model = self.load_model_layers( etl.layers) if res_model: layers_feature_count_before = { name: layer.featureCount() for name, layer in etl.layers.items() } self._running_tool = True self.progress.setVisible(True) res_etl_model = etl.run_etl_model( self.custom_feedback) if not self.custom_feedback.isCanceled( ) and res_etl_model: self.progress.setValue(100) self.button( self.NextButton).setVisible(True) self.button( self.CustomButton1).setVisible(False) self.button(self.CancelButton).setText( QCoreApplication.translate( "SuppliesETLWizard", "Close")) self.show_message( QCoreApplication.translate( "SuppliesETLWizard", "The {} has finished successfully!" ).format(self.tool_name), Qgis.Success, 0) self.logger.clear_status() self.fill_summary( layers_feature_count_before, etl.layers) etl_result = True else: self.initialize_feedback( ) # Get ready for an eventual new execution self.logger.clear_status() self._running_tool = False else: self.show_message(msg_model, Qgis.Warning) else: self.show_message(msg_spatial, Qgis.Warning) else: self.show_message(msg_alpha, Qgis.Warning) else: with OverrideCursor(Qt.WaitCursor): # TODO: if an empty schema was selected, do the magic under the hood # self.create_model_into_database() # Now execute "accepted()" msg = QCoreApplication.translate( "SuppliesETLWizard", "To run the ETL, the database (schema) should have the Supplies LADM-COL structure. Choose a proper database (schema) and try again." ) self.show_message(msg, Qgis.Warning) self.logger.warning(__name__, msg) self.on_result.emit( etl_result) # Inform other classes if the execution was successful def reject(self): if self._running_tool: reply = QMessageBox.question( self, QCoreApplication.translate("SuppliesETLWizard", "Warning"), QCoreApplication.translate( "SuppliesETLWizard", "The '{}' tool is still running. Do you want to cancel it? If you cancel, the data might be incomplete in the target database." ).format(self.tool_name), QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.Yes: self.custom_feedback.cancel() self._running_tool = False msg = QCoreApplication.translate( "SuppliesETLWizard", "The '{}' tool was cancelled.").format(self.tool_name) self.logger.info(__name__, msg) self.show_message(msg, Qgis.Info) else: if self._db_was_changed: self.conn_manager.db_connection_changed.emit( self._db, self._db.test_connection()[0], self.db_source) self.logger.info(__name__, "Dialog closed.") self.app.settings.set_setting( self.app.settings.COBOL_FILES_DIR_KEY, '') self.app.settings.set_setting(self.app.settings.SNC_FILES_DIR_KEY, '') self.done(1) def finished_slot(self, result): self.bar.clearWidgets() def show_message(self, message, level, duration=10): self.bar.clearWidgets( ) # Remove previous messages before showing a new one self.bar.pushMessage(message, level, duration) def fill_summary(self, layers_feature_count_before, etl_layers): layers_feature_count_after = { name: layer.featureCount() for name, layer in etl_layers.items() } summary = """<html><head/><body><p>""" summary += QCoreApplication.translate( "SuppliesETLWizard", "<h4>{} report</h4>").format(self.tool_name) summary += QCoreApplication.translate( "SuppliesETLWizard", "Number of features loaded to the LADM-COL cadastral supplies model:<br/>" ) for name, before_count in layers_feature_count_before.items(): summary += QCoreApplication.translate( "SuppliesETLWizard", '<br/><b>{}</b> : {}'.format( name, layers_feature_count_after[name] - before_count)) summary += """<hr>""" summary += """</body></html>""" self.txt_log.setText(summary) def save_settings(self): settings = QSettings() etl_source = "snc" if self.rad_snc_data.isChecked(): etl_source = "snc" elif self.rad_cobol_data.isChecked(): etl_source = "cobol" settings.setValue('Asistente-LADM-COL/supplies/etl_source', etl_source) self._data_source_widget.save_settings() # In the main page (source-target configuration), save if splitter is closed self.app.settings.etl_splitter_collapsed = self.splitter_2.sizes( )[1] == 0 def restore_settings(self): settings = QSettings() etl_source = settings.value( 'Asistente-LADM-COL/supplies/etl_source') or 'snc' if etl_source == 'snc': self.rad_snc_data.setChecked(True) elif etl_source == 'cobol': self.rad_cobol_data.setChecked(True) # If splitter in the main page was closed before, set it as closed again if self.app.settings.etl_splitter_collapsed: sizes = self.splitter_2.sizes() self.splitter_2.setSizes([sizes[0], 0]) def show_help(self): show_plugin_help('supplies') def show_settings(self): dlg = SettingsDialog(self.conn_manager, parent=self) dlg.setWindowTitle( QCoreApplication.translate("SuppliesETLWizard", "Target DB Connection Settings")) dlg.show_tip( QCoreApplication.translate( "SuppliesETLWizard", "Configure where do you want the data to be imported.")) dlg.set_db_source(self.db_source) dlg.set_required_models([LADMNames.SUPPLIES_MODEL_KEY]) dlg.set_tab_pages_list( [SETTINGS_CONNECTION_TAB_INDEX, SETTINGS_MODELS_TAB_INDEX]) dlg.set_action_type(EnumDbActionType.IMPORT_FROM_ETL) dlg.db_connection_changed.connect(self.db_connection_changed) if self.db_source == COLLECTED_DB_SOURCE: dlg.db_connection_changed.connect( self.app.core.cache_layers_and_relations) if dlg.exec_(): self._db = dlg.get_db_connection() self.update_connection_info() def update_connection_info(self): db_description = self._db.get_description_conn_string() if db_description: self.db_connect_label.setText(db_description) self.db_connect_label.setToolTip( self._db.get_display_conn_string()) else: self.db_connect_label.setText( QCoreApplication.translate("SuppliesETLWizard", "The database is not defined!")) self.db_connect_label.setToolTip('') def db_connection_changed(self, db, ladm_col_db, db_source): # We dismiss parameters here, after all, we already have the db, and the ladm_col_db may change from this moment # until we close the supplies dialog (e.g., we might run an import schema before under the hood) self._db_was_changed = True def load_model_layers(self, layers): self.app.core.get_layers(self._db, layers, load=True) if not layers: return False, QCoreApplication.translate( "SuppliesETLWizard", "There was a problem loading layers from the 'Supplies' model!" ) return True, ''
class STUtils(QObject): def __init__(self): QObject.__init__(self) self.logger = Logger() self.st_session = STSession() self.st_config = TransitionalSystemConfig() def upload_file(self, request_id, supply_type, file_path, comments): url = self.st_config.ST_UPLOAD_FILE_SERVICE_URL.format(request_id) payload = {'typeSupplyId': supply_type, 'observations': comments} files = [('files[]', open(file_path, 'rb'))] headers = { 'Authorization': "Bearer {}".format( self.st_session.get_logged_st_user().get_token()) } msg = "" try: self.logger.debug(__name__, "Uploading file to transitional system...") response = requests.request("PUT", url, headers=headers, data=payload, files=files) except requests.ConnectionError as e: msg = self.st_config.ST_CONNECTION_ERROR_MSG.format(e) self.logger.warning(__name__, msg) return False, msg status_OK = response.status_code == 200 if status_OK: msg = QCoreApplication.translate( "STUtils", "The file was successfully uploaded to the Transitional System!" ) self.logger.success(__name__, msg) else: if response.status_code == 500: msg = self.st_config.ST_STATUS_500_MSG self.logger.warning(__name__, self.st_config.ST_STATUS_500_MSG) elif response.status_code > 500 and response.status_code < 600: msg = self.st_config.ST_STATUS_GT_500_MSG self.logger.warning(__name__, self.st_config.ST_STATUS_GT_500_MSG) elif response.status_code == 401: msg = self.st_config.ST_STATUS_401_MSG self.logger.warning(__name__, self.st_config.ST_STATUS_401_MSG) elif response.status_code == 422: response_data = json.loads(response.text) msg = QCoreApplication.translate( "STUtils", "File was not uploaded! Details: {}").format( response_data['message']) self.logger.warning(__name__, msg) else: msg = QCoreApplication.translate( "STUtils", "Status code not handled: {}").format(response.status_code) self.logger.warning(__name__, msg) return status_OK, msg
class SymbologyUtils(QObject): layer_symbology_changed = pyqtSignal(str) # layer id def __init__(self): QObject.__init__(self) self.logger = Logger() def set_layer_style_from_qml(self, db, layer, is_error_layer=False, emit=False, layer_modifiers=dict(), models=list()): # TODO: Add tests if db is None: self.logger.critical(__name__, "DB connection is none. Style not set.") return qml_name = None if db.is_ladm_layer(layer): layer_name = db.get_ladm_layer_name(layer) else: layer_name = layer.name( ) # we identify some error layer styles using the error table names if not is_error_layer: # Check if we should use modifier style group if LayerConfig.STYLE_GROUP_LAYER_MODIFIERS in layer_modifiers: style_group_modifiers = layer_modifiers.get( LayerConfig.STYLE_GROUP_LAYER_MODIFIERS) if style_group_modifiers: qml_name = style_group_modifiers.get(layer_name) if not qml_name: # If None or empty string, we use default styles qml_name = Symbology().get_default_style_group( db.names, models).get(layer_name) else: style_custom_error_layers = Symbology().get_custom_error_layers() if layer_name in style_custom_error_layers: qml_name = style_custom_error_layers.get(layer_name) else: qml_name = Symbology().get_default_error_style_layer().get( layer.geometryType()) if qml_name: renderer, labeling = self.get_style_from_qml(qml_name) if renderer: layer.setRenderer(renderer) if emit: self.layer_symbology_changed.emit(layer.id()) if labeling: layer.setLabeling(labeling) layer.setLabelsEnabled(True) def get_style_from_qml(self, qml_name): renderer = None labeling = None style_path = os.path.join(STYLES_DIR, qml_name + '.qml') file = QFile(style_path) if not file.open(QIODevice.ReadOnly | QIODevice.Text): self.logger.warning( __name__, "Unable to read style file from {}".format(style_path)) doc = QDomDocument() doc.setContent(file) file.close() doc_elem = doc.documentElement() nodes = doc_elem.elementsByTagName("renderer-v2") if nodes.count(): renderer_elem = nodes.at(0).toElement() renderer = QgsFeatureRenderer.load(renderer_elem, QgsReadWriteContext()) nodes = doc_elem.elementsByTagName("labeling") if nodes.count(): labeling_elem = nodes.at(0).toElement() labeling = QgsAbstractVectorLayerLabeling.create( labeling_elem, QgsReadWriteContext()) return (renderer, labeling)
class QualityRuleEngine(QObject): """ Engine that executes Quality Rules :param db: DBConnector object :param rules: Either a dict {rule_key:rule_name} or a list [rule_key1, rule_key2] :param tolerance: Tolerance to be used when running the QRs, in millimeters """ progress_changed = pyqtSignal(int) # Progress value def __init__(self, db, rules, tolerance, output_path=''): QObject.__init__(self) self.logger = Logger() self.app = AppInterface() self.__qr_registry = QualityRuleRegistry() self.__db = db self.__db_qr = None self.__rules = self.__get_dict_rules(rules) self.__result_layers = list() self.__with_gui = self.app.settings.with_gui self.__timestamp = "" self.__output_path = output_path self.app.settings.tolerance = tolerance # Tolerance must be given, we don't want anything implicit about it self.__tolerance = self.app.settings.tolerance # Tolerance input might be altered (e.g., if it comes negative) self.__layer_manager = QualityRuleLayerManager(db, self.__rules, self.__tolerance) self.__layer_manager.progress_changed.connect( self.__emit_prepare_layers_progress) self.qr_logger = QualityRuleLogger(self.__db, self.__tolerance) self.__current_progress = 0 self.__error_db_utils = QualityErrorDBUtils() self.__error_db_utils.progress_changed.connect( self.__emit_error_db_progress) # Clear informality cache before executing QRs. # Note: between creating an object of this class and calling validate_quality_rules() a lot # of things could happen (like new caches being built!). It is your responsibility to create # an instance of this class or initialize() a QREngine object just before calling validate_quality_rules(). self.app.core.clear_cached_informal_spatial_units() def initialize(self, db, rules, tolerance, output_path='', clear_informality_cache=True): """ Objects of this class are reusable calling initialize() """ self.__result_layers = list() self.__db = db self.__db_qr = None self.__rules = self.__get_dict_rules(rules) self.__with_gui = self.app.settings.with_gui self.__timestamp = "" self.__output_path = output_path self.app.settings.tolerance = tolerance self.__tolerance = self.app.settings.tolerance # Tolerance input might be altered (e.g., if it comes negative) self.__layer_manager.initialize(self.__rules, self.__tolerance) self.qr_logger.initialize(self.__db, self.__tolerance) self.__current_progress = 0 # This time, (initializing an existing object) we give you the chance to avoid # rebuilding the informality cache. It is handy if you're executing validations # consecutively and you're sure that reusing a previous cache does make sense. if clear_informality_cache: self.app.core.clear_cached_informal_spatial_units() def __get_dict_rules(self, rules): if isinstance(rules, dict): return rules # We have everything ready # If rules is a list, we need to retrieve the quality rule names from the QRRegistry return { rule_key: self.__qr_registry.get_quality_rule(rule_key) for rule_key in rules } def validate_quality_rules(self, options=dict()): """ :param options: Dict of dicts with of options per rule. {qr1_key: {qr1_opt1: value1, ...}, ...} """ res = False msg = "" qr_res = dict() # {rule_key: QualityRuleExecutionResult} if self.__rules: self.__emit_progress_changed(1) # First, create the error db and fill its metadata... self.__timestamp = time.strftime('%Y%m%d_%H%M%S') res_db, msg_db, self.__db_qr = self.__error_db_utils.get_quality_error_connector( self.__output_path, self.__timestamp, True) self.__emit_progress_changed(5) if not res_db: msg_db = QCoreApplication.translate( "QualityRuleEngine", "There was a problem creating the quality error DB! Details: {}" ).format(msg_db) self.logger.warning(__name__, msg_db) return False, msg_db, None self.qr_logger.set_count_topology_rules(len(self.__rules)) self.logger.info( __name__, QCoreApplication.translate( "QualityRuleEngine", "Validating {} quality rules (tolerance: {}).").format( len(self.__rules), self.__tolerance)) first_pass = True count = 0 for rule_key, rule in self.__rules.items(): count += 1 if rule is not None: layers = self.__layer_manager.get_layers( rule_key) # Fist pass might be long if tolerance > 0 if first_pass: first_pass = False self.__emit_progress_changed( 25 if self.__tolerance else 15) if layers: connect_obj = rule.progress_changed.connect( partial(self.__emit_qr_progress, count)) qr_res[rule_key] = self.__validate_quality_rule( rule, layers, options.get(rule_key, dict())) rule.progress_changed.disconnect( connect_obj ) # We no longer need the connection, so delete it else: qr_msg = QCoreApplication.translate( "QualityRuleEngine", "Couldn't execute '{}' quality rule! Required layers are not available. Skipping..." ).format(rule.name()) qr_res[rule_key] = QualityRuleExecutionResult( EnumQualityRuleResult.CRITICAL, qr_msg) self.logger.warning(__name__, qr_msg) else: qr_msg = QCoreApplication.translate( "QualityRuleEngine", "Quality rule with key '{}' does not exist or is not registered! Skipping..." ).format(rule_key) qr_res[rule_key] = QualityRuleExecutionResult( EnumQualityRuleResult.CRITICAL, qr_msg) self.logger.warning(__name__, qr_msg) self.__emit_progress_changed(95) metadata = { QR_METADATA_TOOL: QR_METADATA_TOOL_NAME, QR_METADATA_DATA_SOURCE: self.__db.get_description_conn_string(), QR_METADATA_TOLERANCE: self.__tolerance / 1000, QR_METADATA_TIMESTAMP: self.__timestamp, QR_METADATA_RULES: list(self.__rules.keys()), # QR keys QR_METADATA_OPTIONS: self.__normalize_options(options), QR_METADATA_PERSON: getpass.getuser() } QualityErrorDBUtils.save_metadata(self.__db_qr, metadata) self.export_result_to_pdf() self.__emit_progress_changed(99) res = True msg = "Success!" self.__layer_manager.clean_temporary_layers() else: self.logger.warning( __name__, QCoreApplication.translate("QualityRuleEngine", "No rules to validate!")) self.__emit_progress_changed(100) return res, msg, QualityRulesExecutionResult(qr_res) @_log_quality_rule_validations def __validate_quality_rule(self, rule, layers, options): """ Intermediate function to log quality rule execution. :param rule: Quality rule instance :param layers: Layer dict with the layers the quality rule needs (ready to use for tolerance > 0 scenarios) :param options: Dict of options per rule. {qr1_opt1: value1, ...} :return: An instance of QualityRuleExecutionResult """ return rule.validate(self.__db, self.__db_qr, layers, self.__tolerance, options=options) def __emit_progress_changed(self, value, save_value=True): if value != self.__current_progress: # Avoid emitting the same value twice if save_value: self.__current_progress = value self.progress_changed.emit(value) def __emit_error_db_progress(self, progress_value): """ Add the normalized error db progress value to what we have already in the overall progress """ value = self.__current_progress + (progress_value * TOTAL_PROGRESS_ERROR_DB / 100) # print("...DB", self.__current_progress, progress_value, value) self.__emit_progress_changed( int(value), progress_value == 100) # Only save when the subprocess is finished def __emit_prepare_layers_progress(self, progress_value): """ Add the normalized prepare layers' progress value to what we have already in the overall progress """ range = TOTAL_PROGRESS_PREPARE_LAYERS if self.__tolerance else TOTAL_PROGRESS_PREPARE_LAYERS_NO_TOLERANCE value = self.__current_progress + (progress_value * range / 100) # print("...PL", self.__current_progress, progress_value, value) self.__emit_progress_changed( int(value), progress_value == 100) # Only save when the subprocess is finished def __emit_qr_progress(self, count, qr_progress_value): """ Add the normalized current QR progress value to what we have already in the overall progress """ num_rules = len(self.__rules) step = (TOTAL_PROGRESS_QR if self.__tolerance else TOTAL_PROGRESS_QR_NO_TOLERANCE) / num_rules value = self.__current_progress + (qr_progress_value * step / 100) # print("...QR", self.__current_progress, qr_progress_value, value) self.__emit_progress_changed( int(value), qr_progress_value == 100) # Only save when the QR is finished def get_db_quality(self): return self.__db_qr def get_timestamp(self): # Last timestamp used to validate QRs. # Note the timestamp is persisted in QR DB's metadata table return self.__timestamp def export_result_to_pdf(self): res, msg, output_path = QualityErrorDBUtils.get_quality_validation_output_path( self.__output_path, self.__timestamp) if not res: self.logger.critical( __name__, QCoreApplication.translate( "QualityRuleEngine", "PDF report could not be exported, there were problems with the output path '{}'!" ).format(self.__output_path)) return pdf_path = os.path.join( output_path, "Reglas_de_Calidad_{}.pdf".format(self.__timestamp)) log = self.qr_logger.get_log_result() export_title_text_to_pdf(pdf_path, log.title, log.text) def __normalize_options(self, options): # Get rid of options that do not correspond to validated QRs normalized_options = dict() for k, v in options.items(): for kv, vv in v.items(): if k in list(self.__rules.keys()): if k in normalized_options: normalized_options[k][kv] = vv else: normalized_options[k] = {kv: vv} return normalized_options
class SymbologyUtils(QObject): layer_symbology_changed = pyqtSignal(str) # layer id def __init__(self): QObject.__init__(self) self.logger = Logger() self.symbology = Symbology() def set_layer_style_from_qml(self, db, layer, is_error_layer=False, emit=False, layer_modifiers=dict()): style_group = self.symbology.get_default_style_group(db.names) style_group_const = self.symbology.get_default_style_group(db.names) if LayerConfig.STYLE_GROUP_LAYER_MODIFIERS in layer_modifiers: if layer_modifiers[LayerConfig.STYLE_GROUP_LAYER_MODIFIERS]: style_group = layer_modifiers[LayerConfig.STYLE_GROUP_LAYER_MODIFIERS] qml_name = None if is_error_layer: if layer.name() in self.symbology.get_custom_error_layers(): # Symbology is selected according to the language if QGIS_LANG in self.symbology.get_custom_error_layers()[layer.name()]: qml_name = self.symbology.get_custom_error_layers()[layer.name()][QGIS_LANG] else: qml_name = self.symbology.get_custom_error_layers()[layer.name()][DEFAULT_LANGUAGE] else: qml_name = style_group_const[self.symbology.get_error_layer_name()][layer.geometryType()] else: if db is None: return layer_name = db.get_ladm_layer_name(layer) if layer_name in style_group: if layer.geometryType() in style_group[layer_name]: qml_name = style_group[layer_name][layer.geometryType()] # If style not in style group then we use default simbology if qml_name is None: if layer_name in style_group_const: if layer.geometryType() in style_group_const[layer_name]: qml_name = style_group_const[layer_name][layer.geometryType()] if qml_name is not None: renderer, labeling = self.get_style_from_qml(qml_name) if renderer: layer.setRenderer(renderer) if emit: self.layer_symbology_changed.emit(layer.id()) if labeling: layer.setLabeling(labeling) layer.setLabelsEnabled(True) def get_style_from_qml(self, qml_name): renderer = None labeling = None style_path = os.path.join(STYLES_DIR, qml_name + '.qml') file = QFile(style_path) if not file.open(QIODevice.ReadOnly | QIODevice.Text): self.logger.warning(__name__, "Unable to read style file from {}".format(style_path)) doc = QDomDocument() doc.setContent(file) file.close() doc_elem = doc.documentElement() nodes = doc_elem.elementsByTagName("renderer-v2") if nodes.count(): renderer_elem = nodes.at(0).toElement() renderer = QgsFeatureRenderer.load(renderer_elem, QgsReadWriteContext()) nodes = doc_elem.elementsByTagName("labeling") if nodes.count(): labeling_elem = nodes.at(0).toElement() labeling = QgsAbstractVectorLayerLabeling.create(labeling_elem, QgsReadWriteContext()) return (renderer, labeling)
class GUIBuilder(QObject): """ Build plugin GUI according to roles and LADM-COL models present in the current db connection """ def __init__(self, iface): QObject.__init__(self) self.iface = iface self.logger = Logger() self._registered_actions = dict() self._registered_dock_widgets = dict() self.menus = list() self.toolbar_menus = list() self.toolbars = list() # When building the GUI we rely on info from the DB connection self._db = None self._test_conn_result = None self._db_engine_actions = list() self._engine_name = "" def register_action(self, key, action): self._registered_actions[key] = { ACTION: action, DEFAULT_ACTION_TEXT: action.text(), DEFAULT_ACTION_STATUS: action.isEnabled() } def register_actions(self, dict_key_action): new_dict = dict() for k, v in dict_key_action.items(): new_dict[k] = { ACTION: v, DEFAULT_ACTION_TEXT: v.text(), DEFAULT_ACTION_STATUS: v.isEnabled() } self._registered_actions.update(new_dict) del new_dict def unregister_actions(self, list_keys): """ Mainly for plugins that add functionalities as add-ons :param list_keys: List of action keys """ b_any_change = False for action_key in list_keys: if action_key in self._registered_actions: b_any_change = True self.logger.info( __name__, "Unregistering action '{}'...".format(action_key)) # del self._registered_actions[action_key][ACTION] # Leave this to the add-on del self._registered_actions[action_key] if b_any_change: self.build_gui( ) # Refresh the GUI, since removing actions might leave some menus unnecessary def get_action(self, action_key): return self._get_and_configure_action(action_key) def register_dock_widget(self, key, dock_widget): self._registered_dock_widgets[key] = dock_widget def set_db_connection(self, db, test_conn_result=None): """ Set the DB connection info this class will use to build the GUI. :param db: DBConnector object :param test_conn_result: Can be True or False if test_connection was called, or None if we should call it. :return: """ self._db = db if test_conn_result is not None: self._test_conn_result = test_conn_result else: self._test_conn_result, code, msg = db.test_connection() if not self._test_conn_result: self.logger.warning( __name__, "Test connection is False! Details: {}".format(msg)) db_factory = ConfigDBsSupported().get_db_factory(db.engine) self._db_engine_actions = db_factory.get_db_engine_actions() self._engine_name = db_factory.get_name() def build_gui(self): """ Build the plugin gui according to configurations. We first check if the DB is LADM, if not, we use a default gui configuration. Otherwise we ask the role_key for a gui configuration. If he/she has it, we use it, otherwise we use a template gui configuration. """ if self._db is None or self._test_conn_result is None: self.logger.warning( __name__, QCoreApplication.translate( "AsistenteLADMCOLPlugin", "You should first set the db connection in the GUI_Builder to build the GUI!" )) return self.unload_gui(final_unload=False) # First clear everything # Filter menus and actions and get a gui_config with the proper structure ready to build the GUI (e.g., with no # empty Menus) gui_config = self._get_filtered_gui_config() for component, values in gui_config.items(): if component == MAIN_MENU: for menu_def in values: menu = self._build_menu(menu_def) # Try to add the menu in the second to last position of the QGIS menus existent_actions = self.iface.mainWindow().menuBar( ).actions() if len(existent_actions) > 0: last_action = existent_actions[-1] self.iface.mainWindow().menuBar().insertMenu( last_action, menu) else: self.iface.mainWindow().menuBar().addMenu(menu) self.menus.append(menu) elif component == TOOLBAR: for toolbar_def in values: # We expect a list of dicts here... toolbar = self._build_toolbar(toolbar_def) self.toolbars.append(toolbar) def _get_filtered_gui_config(self): """ Rebuilds a gui_config dict removing not allowed actions. :return: Dictionary in the form of a gui_config dict, but only with allowed actions for the role_key passed. """ role_key = RoleRegistry().get_active_role() self.logger.info( __name__, "Active role: {}".format(RoleRegistry().get_role_name(role_key))) gui_config = self._get_gui_config(role_key) # self.logger.debug(__name__, "Filtered gui_config: {}".format(gui_config)) role_actions = self._get_role_actions(role_key) model_actions = self._get_model_actions( ) if self._test_conn_result else list() # If you want to take models into account, combine role_actions and model_actions as you like, and store the # result in allowed_actions. # # Here we define how to deal with actions, role permissions and models present # We decided to prefer always the rol's actions. Like this (R: Role, M: Model, Res: Result): # R M Res # V V V # V F V # F V F # F F F # # Therefore: allowed_actions = role_actions # It's safe to make use of this list, no need to copy it, as it's a sum of lists self.logger.debug( __name__, "Allowed actions for role '{}': {}".format(role_key, allowed_actions)) # Now, use only allowed actions and remove other actions from gui_config filtered_gui_config = dict() for k, v in gui_config.items(): if k == MAIN_MENU or k == TOOLBAR: for menu_def in v: actions = self._get_filtered_actions( menu_def[ACTIONS], allowed_actions) if actions: menu_def[ACTIONS] = actions if not k in filtered_gui_config: filtered_gui_config[k] = [menu_def] else: filtered_gui_config[k].append(menu_def) return filtered_gui_config def _get_filtered_actions(self, action_list, allowed_actions): """ Filters out not allowed actions from an action list. It removes menus if no actions are allowed inside that menu, and it also removes separators if they are in a wrong position (e.e., two consecutive separators, a trailing separator, etc.) :param action_list: List of all actions defined in a gui_config dict. :param allowed_actions: List of allowed actions. Actions that are not here are not returned by this function. :return: List of actions with actions not allowed removed. """ filtered_actions = list() for item in action_list: if type(item) is dict: # Menu menu_actions = self._get_filtered_actions( item[ACTIONS], allowed_actions) if [ menu_action for menu_action in menu_actions if menu_action != SEPARATOR ]: item[ACTIONS] = menu_actions filtered_actions.append(item) elif item == SEPARATOR: if filtered_actions and filtered_actions[-1] != SEPARATOR: filtered_actions.append(SEPARATOR) else: # Action if (item in allowed_actions or ALL_ACTIONS in allowed_actions ) and item in self._registered_actions: # The action must be registered, otherwise we don't continue # If the action is registered, we check if the action is allowed, either by finding ALL_ACTIONS # or by finding the action in the allowed actions list filtered_actions.append(item) self._remove_trailing_separators(filtered_actions) return filtered_actions def _remove_trailing_separators(self, action_list): """ Remove unnecessary trailing separators, both in menus and in the current action_list. Modifies the input list. :param action_list: list of actions, separators and other widgets """ for item in action_list[:]: if type(item) is dict: # We don't expect empty ACTION lists, so it should be safe a [-1] if item[ACTIONS][-1] == SEPARATOR: del item[ACTIONS][-1] if action_list and action_list[-1] == SEPARATOR: del action_list[-1] def _get_gui_config(self, role_key): """ Get a basic GUI config (still unfiltered). :param role_key: Active role key to whom we will ask for its GUI config. Normally, it should be the active one. :return: Dictionary in the form of a gui_config dict (still unfiltered). """ if self._test_conn_result: self.logger.info(__name__, "Using template gui_config from the role.") return RoleRegistry().get_role_gui_config(role_key) else: default_gui = RoleRegistry().get_role_gui_config( role_key, DEFAULT_GUI) if default_gui: self.logger.info( __name__, "Using default gui_config (minimal) from the role.") return default_gui else: self.logger.info( __name__, "Using gui_config from the default GUI (minimal).") return GUI_Config().get_gui_dict( DEFAULT_GUI ) # Use a default gui config given by the plugin def _get_role_actions(self, role_key): """ Get actions a given role has access to. :param role_key: Role key. :return: List of actions a role has access to. """ return RoleRegistry().get_role_actions(role_key) def _get_model_actions(self): """ Gets a list of actions that models in the DB enable. E.g., if we have valuation model, we add to this list valuation actions, otherwise we don't. :return: List of actions without duplicate elements. """ actions = list() if self._db.survey_model_exists(): actions.extend(MODELS_GUI_DICT[LADMNames.SURVEY_MODEL_KEY]) if self._db.valuation_model_exists(): actions.extend(MODELS_GUI_DICT[LADMNames.VALUATION_MODEL_KEY]) return list(set(actions)) def unload_gui(self, final_unload=True): """ Destroys the GUI (Menus and toolbars) :param final_unload: True if the plugin is closing. False if we just destroy the GUI to rebuild it once more. """ if final_unload: self.logger.info(__name__, "Unloading completely the GUI (final_unload)") for action_info in self._registered_actions.values(): del action_info[ACTION] self._registered_actions = dict() for menu in self.menus: menu.clear() menu.deleteLater() for menu in self.toolbar_menus: # Basically, a push button who has received a menu menu.deleteLater() for toolbar in self.toolbars: self.iface.mainWindow().removeToolBar(toolbar) del toolbar self.menus = list() self.toolbar_menus = list() self.toolbars = list() # Make sure dock widgets are deleted properly self.close_dock_widgets(list(self._registered_dock_widgets.keys())) self.logger.info(__name__, "GUI unloaded (not a final_unload)") def close_dock_widgets(self, dock_widget_keys): """ Deletes properly registered dock widgets by key :param dock_widget_keys: List of dock widget keys to delete """ for dock_widget_key in dock_widget_keys: if dock_widget_key in self._registered_dock_widgets: if self._registered_dock_widgets[dock_widget_key] is not None: self.logger.info( __name__, "Deleting dock widget '{}'...".format(dock_widget_key)) self._registered_dock_widgets[dock_widget_key].close() self._registered_dock_widgets[dock_widget_key] = None def _build_menu(self, menu_def): menu = self.iface.mainWindow().findChild(QMenu, menu_def[OBJECT_NAME]) if menu is None: menu = QMenu(menu_def[WIDGET_NAME], self.iface.mainWindow().menuBar()) if ICON in menu_def: menu.setIcon(QIcon(menu_def[ICON])) menu.setObjectName(menu_def[OBJECT_NAME]) self._build_actions(menu_def[ACTIONS], menu) return menu def _build_toolbar_menu(self, menu_def): # Menus for toolbars are created differently... widget = self.iface.mainWindow().findChild(QPushButton, menu_def[OBJECT_NAME]) if widget is None: widget = QPushButton(menu_def[WIDGET_NAME]) menu = QMenu() if ICON in menu_def: widget.setIcon(QIcon(menu_def[ICON])) widget.setMenu(menu) self.menus.append( menu ) # Because menu ownership is not transferred to the push button! self._build_actions( menu_def[ACTIONS], menu) # Now we have a normal menu, build actions on it return widget def _build_toolbar(self, toolbar_def): toolbar = self.iface.mainWindow().findChild(QToolBar, toolbar_def[OBJECT_NAME]) if toolbar is None: toolbar = self.iface.addToolBar( QCoreApplication.translate("AsistenteLADMCOLPlugin", toolbar_def[WIDGET_NAME])) toolbar.setObjectName(toolbar_def[OBJECT_NAME]) toolbar.setToolTip(toolbar_def[WIDGET_NAME]) self._build_toolbar_actions(toolbar_def[ACTIONS], toolbar) return toolbar def _build_actions(self, actions_list, base_menu): for item in actions_list: if type(item) is dict: # Menu menu = self._build_menu(item) base_menu.addMenu(menu) self.menus.append(menu) elif item == SEPARATOR: base_menu.addSeparator() else: # Action if item in self._registered_actions: base_menu.addAction(self._get_and_configure_action(item)) def _build_toolbar_actions(self, actions_list, toolbar): for item in actions_list: if type(item) is dict: # Menu widget = self._build_toolbar_menu(item) toolbar.addWidget(widget) self.toolbar_menus.append(widget) elif item == SEPARATOR: toolbar.addSeparator() else: # Action if item in self._registered_actions: toolbar.addAction(self._get_and_configure_action(item)) def _get_and_configure_action(self, action_key): """ Get and configure actions. Configuration means to enable/disable them, and set text and tooltip, among others. :param action_key: :return: Configured QAction """ action = self._registered_actions[action_key][ ACTION] if action_key in self._registered_actions else None if action is None: return action # Default properties action_text = self._registered_actions[action_key][DEFAULT_ACTION_TEXT] action.setEnabled( self._registered_actions[action_key][DEFAULT_ACTION_STATUS]) action.setText(action_text) action.setToolTip(action_text) # If not supported by current DB engine... if not ALL_ACTIONS in self._db_engine_actions and not action_key in self._db_engine_actions: action.setEnabled(False) action.setText( QCoreApplication.translate("AsistenteLADMCOLPlugin", "{} (not for {})").format( action_text, self._engine_name)) action.setToolTip( QCoreApplication.translate( "AsistenteLADMCOLPlugin", "Not supported by {}".format(self._engine_name))) return action def show_welcome_screen(self): return not RoleRegistry().active_role_already_set() def add_actions_to_db_engine(self, action_key_list): """ For add-ons to add a set of actions to current DB engine. After a call to this method, it is expected a build_gui to refresh the GUI. :param action_key_list: List of action keys that should be added to current db engine """ self._db_engine_actions = list( set(self._db_engine_actions + action_key_list))
class CreatePointsSurveyWizard(QWizard, WIZARD_UI): WIZARD_NAME = "CreatePointsSurveyWizard" WIZARD_TOOL_NAME = QCoreApplication.translate(WIZARD_NAME, "Create Point") def __init__(self, iface, db): QWizard.__init__(self) self.setupUi(self) self.iface = iface self._db = db self.logger = Logger() self.app = AppInterface() self.names = self._db.names self.help_strings = HelpStrings() self._layers = { self.names.LC_BOUNDARY_POINT_T: None, self.names.LC_SURVEY_POINT_T: None, self.names.LC_CONTROL_POINT_T: None } self.target_layer = None # Auxiliary data to set nonlinear next pages self.pages = [self.wizardPage1, self.wizardPage2, self.wizardPage3] self.dict_pages_ids = {self.pages[idx] : pid for idx, pid in enumerate(self.pageIds())} self.mMapLayerComboBox.setFilters(QgsMapLayerProxyModel.PointLayer) # Set connections self.btn_browse_file.clicked.connect( make_file_selector(self.txt_file_path, file_filter=QCoreApplication.translate("WizardTranslations",'CSV File (*.csv *.txt)'))) self.txt_file_path.textChanged.connect(self.file_path_changed) self.crsSelector.crsChanged.connect(self.crs_changed) self.crs = "" # SRS auth id self.txt_delimiter.textChanged.connect(self.fill_long_lat_combos) self.mMapLayerComboBox.layerChanged.connect(self.import_layer_changed) self.known_delimiters = [ {'name': ';', 'value': ';'}, {'name': ',', 'value': ','}, {'name': 'tab', 'value': '\t'}, {'name': 'space', 'value': ' '}, {'name': '|', 'value': '|'}, {'name': '~', 'value': '~'}, {'name': 'Other', 'value': ''} ] self.cbo_delimiter.addItems([ item['name'] for item in self.known_delimiters ]) self.cbo_delimiter.currentTextChanged.connect(self.separator_changed) self.restore_settings() self.txt_file_path.textChanged.emit(self.txt_file_path.text()) self.rad_boundary_point.toggled.connect(self.point_option_changed) self.rad_control_point.toggled.connect(self.point_option_changed) self.rad_csv.toggled.connect(self.adjust_page_2_controls) self.point_option_changed() # Initialize it self.button(QWizard.FinishButton).clicked.connect(self.finished_dialog) self.currentIdChanged.connect(self.current_page_changed) self.txt_help_page_2.setHtml(self.help_strings.WIZ_ADD_POINTS_SURVEY_PAGE_2_OPTION_CSV) self.wizardPage2.setButtonText(QWizard.FinishButton, QCoreApplication.translate("WizardTranslations", "Import")) self.txt_help_page_3.setHtml(self.help_strings.WIZ_ADD_POINTS_SURVEY_PAGE_3_OPTION_CSV) self.txt_help_page_3.anchorClicked.connect(self.save_template) self.button(QWizard.HelpButton).clicked.connect(self.show_help) self.rejected.connect(self.close_wizard) # Set MessageBar for QWizard self.bar = QgsMessageBar() self.bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.setLayout(QGridLayout()) self.layout().addWidget(self.bar, 0, 0, Qt.AlignTop) def nextId(self): """ Set navigation order. Should return an integer. -1 is Finish. """ if self.currentId() == self.dict_pages_ids[self.wizardPage1]: return self.dict_pages_ids[self.wizardPage2] elif self.currentId() == self.dict_pages_ids[self.wizardPage2]: if self.rad_csv.isChecked(): return self.dict_pages_ids[self.wizardPage3] elif self.rad_refactor.isChecked(): return -1 elif self.currentId() == self.dict_pages_ids[self.wizardPage3]: return -1 else: return -1 def current_page_changed(self, id): """ Reset the Next button. Needed because Next might have been disabled by a condition in a another SLOT. """ enable_next_wizard(self) if id == self.dict_pages_ids[self.wizardPage2]: self.adjust_page_2_controls() elif id == self.dict_pages_ids[self.wizardPage3]: self.set_buttons_visible(False) self.set_buttons_enabled(False) QCoreApplication.processEvents() self.check_z_in_geometry() QCoreApplication.processEvents() self.fill_long_lat_combos("") QCoreApplication.processEvents() self.set_buttons_visible(True) self.set_buttons_enabled(True) def set_buttons_visible(self, visible): self.button(self.BackButton).setVisible(visible) self.button(self.FinishButton).setVisible(visible) self.button(self.CancelButton).setVisible(visible) def set_buttons_enabled(self, enabled): self.wizardPage3.setEnabled(enabled) self.button(self.BackButton).setEnabled(enabled) self.button(self.FinishButton).setEnabled(enabled) self.button(self.CancelButton).setEnabled(enabled) def check_z_in_geometry(self): self.target_layer = self.app.core.get_layer(self._db, self.current_point_name(), load=True) if not self.target_layer: return if not QgsWkbTypes().hasZ(self.target_layer.wkbType()): self.labelZ.setEnabled(False) self.cbo_elevation.setEnabled(False) msg = QCoreApplication.translate("WizardTranslations", "The current model does not support 3D geometries") self.cbo_elevation.setToolTip(msg) self.labelZ.setToolTip(msg) else: self.labelZ.setEnabled(True) self.cbo_elevation.setEnabled(True) self.labelZ.setToolTip("") self.cbo_elevation.setToolTip("") def adjust_page_2_controls(self): self.cbo_mapping.clear() self.cbo_mapping.addItem("") self.cbo_mapping.addItems(self.app.core.get_field_mappings_file_names(self.current_point_name())) if self.rad_refactor.isChecked(): self.lbl_refactor_source.setEnabled(True) self.mMapLayerComboBox.setEnabled(True) self.lbl_field_mapping.setEnabled(True) self.cbo_mapping.setEnabled(True) self.import_layer_changed(self.mMapLayerComboBox.currentLayer()) disable_next_wizard(self) self.wizardPage2.setFinalPage(True) self.txt_help_page_2.setHtml(self.help_strings.get_refactor_help_string(self._db, self._layers[self.current_point_name()])) elif self.rad_csv.isChecked(): self.lbl_refactor_source.setEnabled(False) self.mMapLayerComboBox.setEnabled(False) self.lbl_field_mapping.setEnabled(False) self.cbo_mapping.setEnabled(False) self.lbl_refactor_source.setStyleSheet('') enable_next_wizard(self) self.wizardPage2.setFinalPage(False) self.txt_help_page_2.setHtml(self.help_strings.WIZ_ADD_POINTS_SURVEY_PAGE_2_OPTION_CSV) def point_option_changed(self): if self.rad_boundary_point.isChecked(): self.gbx_page_2.setTitle(QCoreApplication.translate("WizardTranslations", "Load data to Boundary Points...")) self.gbx_page_3.setTitle(QCoreApplication.translate("WizardTranslations", "Configure CSV data source for Boundary Points...")) self.txt_help_page_1.setHtml(self.help_strings.WIZ_ADD_POINTS_SURVEY_PAGE_1_OPTION_BP) elif self.rad_survey_point.isChecked(): # self.rad_survey_point is checked self.gbx_page_2.setTitle(QCoreApplication.translate("WizardTranslations", "Load data to Survey Points...")) self.gbx_page_3.setTitle(QCoreApplication.translate("WizardTranslations", "Configure CSV data source for Survey Points...")) self.txt_help_page_1.setHtml(self.help_strings.WIZ_ADD_POINTS_SURVEY_PAGE_1_OPTION_SP) else: # self.rad_control_point is checked self.gbx_page_2.setTitle(QCoreApplication.translate("WizardTranslations", "Load data to Control Points...")) self.gbx_page_3.setTitle(QCoreApplication.translate("WizardTranslations", "Configure CSV data source for Control Points...")) self.txt_help_page_1.setHtml(self.help_strings.WIZ_ADD_POINTS_SURVEY_PAGE_1_OPTION_CP) def finished_dialog(self): self.save_settings() if self.rad_refactor.isChecked(): output_layer_name = self.current_point_name() if self.mMapLayerComboBox.currentLayer() is not None: field_mapping = self.cbo_mapping.currentText() res_etl_model = self.app.core.show_etl_model(self._db, self.mMapLayerComboBox.currentLayer(), output_layer_name, field_mapping=field_mapping) if res_etl_model: self.app.gui.redraw_all_layers() # Redraw all layers to show imported data # If the result of the etl_model is successful and we used a stored recent mapping, we delete the # previous mapping used (we give preference to the latest used mapping) if field_mapping: self.app.core.delete_old_field_mapping(field_mapping) self.app.core.save_field_mapping(output_layer_name) else: self.logger.warning_msg(__name__, QCoreApplication.translate("WizardTranslations", "Select a source layer to set the field mapping to '{}'.").format(output_layer_name)) self.close_wizard() elif self.rad_csv.isChecked(): self.prepare_copy_csv_points_to_db() def close_wizard(self, message=None, show_message=True): if message is None: message = QCoreApplication.translate("WizardTranslations", "'{}' tool has been closed.").format(self.WIZARD_TOOL_NAME) if show_message: self.logger.info_msg(__name__, message) self.close() def current_point_name(self): if self.rad_boundary_point.isChecked(): return self.names.LC_BOUNDARY_POINT_T elif self.rad_survey_point.isChecked(): return self.names.LC_SURVEY_POINT_T else: return self.names.LC_CONTROL_POINT_T def prepare_copy_csv_points_to_db(self): csv_path = self.txt_file_path.text().strip() if not csv_path or not os.path.exists(csv_path): self.logger.warning_msg(__name__, QCoreApplication.translate("WizardTranslations", "No CSV file given or file doesn't exist.")) return target_layer_name = self.current_point_name() with OverrideCursor(Qt.WaitCursor): csv_layer = self.app.core.csv_to_layer(csv_path, self.txt_delimiter.text(), self.cbo_longitude.currentText(), self.cbo_latitude.currentText(), self.crs, self.cbo_elevation.currentText() or None, self.detect_decimal_point(csv_path)) self.app.core.copy_csv_to_db(csv_layer, self._db, target_layer_name) def required_layers_are_available(self): layers_are_available = self.app.core.required_layers_are_available(self._db, self._layers, self.WIZARD_TOOL_NAME) return layers_are_available def file_path_changed(self): self.autodetect_separator() self.fill_long_lat_combos("") self.cbo_delimiter.currentTextChanged.connect(self.separator_changed) def detect_decimal_point(self, csv_path): if os.path.exists(csv_path): with open(csv_path) as file: file.readline() # headers data = file.readline().strip() # 1st line with data if data: fields = self.get_fields_from_csv_file(csv_path) if self.cbo_latitude.currentText() in fields: num_col = data.split(self.cbo_delimiter.currentText())[fields.index(self.cbo_latitude.currentText())] for decimal_point in ['.', ',']: if decimal_point in num_col: return decimal_point return '.' # just use the default one def autodetect_separator(self): csv_path = self.txt_file_path.text().strip() if os.path.exists(csv_path): with open(csv_path) as file: first_line = file.readline() for delimiter in self.known_delimiters: if delimiter['value'] == '': continue # if separator works like a column separator in header # number of cols is greater than 1 if len(first_line.split(delimiter['value'])) > 1: self.cbo_delimiter.setCurrentText(delimiter['name']) return def update_crs_info(self): self.crsSelector.setCrs(QgsCoordinateReferenceSystem(self.crs)) def crs_changed(self): self.crs = get_crs_authid(self.crsSelector.crs()) if self.crs != DEFAULT_SRS_AUTHID: self.lbl_crs.setStyleSheet('color: orange') self.lbl_crs.setToolTip(QCoreApplication.translate("WizardTranslations", "Your CSV data will be reprojected for you to '{}' (Colombian National Origin),<br>before attempting to import it into LADM-COL.").format(DEFAULT_SRS_AUTHID)) else: self.lbl_crs.setStyleSheet('') self.lbl_crs.setToolTip(QCoreApplication.translate("WizardTranslations", "Coordinate Reference System")) def fill_long_lat_combos(self, text): csv_path = self.txt_file_path.text().strip() self.cbo_longitude.clear() self.cbo_latitude.clear() self.cbo_elevation.clear() if os.path.exists(csv_path): self.button(QWizard.FinishButton).setEnabled(True) fields = self.get_fields_from_csv_file(csv_path) fields_dict = {field: field.lower() for field in fields} if not fields: self.button(QWizard.FinishButton).setEnabled(False) return self.cbo_longitude.addItems(fields) self.cbo_latitude.addItems(fields) self.cbo_elevation.addItems([""] + fields) # Heuristics to suggest values for x, y and z x_potential_names = ['x', 'lon', 'long', 'longitud', 'longitude', 'este', 'east', 'oeste', 'west'] y_potential_names = ['y', 'lat', 'latitud', 'latitude', 'norte', 'north'] z_potential_names = ['z', 'altura', 'elevacion', 'elevation', 'elevación', 'height'] for x_potential_name in x_potential_names: for k,v in fields_dict.items(): if x_potential_name == v: self.cbo_longitude.setCurrentText(k) break for y_potential_name in y_potential_names: for k, v in fields_dict.items(): if y_potential_name == v: self.cbo_latitude.setCurrentText(k) break if self.cbo_elevation.isEnabled(): for z_potential_name in z_potential_names: for k, v in fields_dict.items(): if z_potential_name == v: self.cbo_elevation.setCurrentText(k) break else: self.button(QWizard.FinishButton).setEnabled(False) def get_fields_from_csv_file(self, csv_path): if not self.txt_delimiter.text(): return [] error_reading = False try: reader = open(csv_path, "r") except IOError: error_reading = True line = reader.readline().replace("\n", "") reader.close() if not line: error_reading = True else: return line.split(self.txt_delimiter.text()) if error_reading: self.logger.warning_msg(__name__, QCoreApplication.translate("WizardTranslations", "It was not possible to read field names from the CSV. Check the file and try again.")) return [] def separator_changed(self, text): # first ocurrence value = next((x['value'] for x in self.known_delimiters if x['name'] == text), '') self.txt_delimiter.setText(value) if value == '': self.txt_delimiter.setEnabled(True) else: self.txt_delimiter.setEnabled(False) def save_template(self, url): link = url.url() if self.rad_boundary_point.isChecked(): if link == '#template': self.download_csv_file('template_boundary_points.csv') elif link == '#data': self.download_csv_file('sample_boundary_points.csv') elif self.rad_survey_point.isChecked(): if link == '#template': self.download_csv_file('template_survey_points.csv') elif link == '#data': self.download_csv_file('sample_survey_points.csv') elif self.rad_control_point.isChecked(): if link == '#template': self.download_csv_file('template_control_points.csv') elif link == '#data': self.download_csv_file('sample_control_points.csv') def download_csv_file(self, filename): settings = QSettings() settings.setValue('Asistente-LADM-COL/wizards/points_csv_file_delimiter', self.txt_delimiter.text().strip()) new_filename, filter = QFileDialog.getSaveFileName(self, QCoreApplication.translate("WizardTranslations", "Save File"), os.path.join(settings.value('Asistente-LADM-COL/wizards/points_download_csv_path', '.'), filename), QCoreApplication.translate("WizardTranslations", "CSV File (*.csv *.txt)")) if new_filename: settings.setValue('Asistente-LADM-COL/wizards/points_download_csv_path', os.path.dirname(new_filename)) template_file = QFile(":/Asistente-LADM-COL/resources/csv/" + filename) if not template_file.exists(): self.logger.critical(__name__, "CSV doesn't exist! Probably due to a missing 'make' execution to generate resources...") msg = QCoreApplication.translate("WizardTranslations", "CSV file not found. Update your plugin. For details see log.") self.show_message(msg, Qgis.Warning) return if os.path.isfile(new_filename): self.logger.info(__name__, 'Removing existing file {}...'.format(new_filename)) os.chmod(new_filename, 0o777) os.remove(new_filename) if template_file.copy(new_filename): os.chmod(new_filename, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH) msg = QCoreApplication.translate("WizardTranslations", """The file <a href="file:///{}">{}</a> was successfully saved!""").format(normalize_local_url(new_filename), os.path.basename(new_filename)) self.show_message(msg, Qgis.Info) else: self.logger.warning(__name__, 'There was an error copying the CSV file {}!'.format(new_filename)) msg = QCoreApplication.translate("WizardTranslations", "The file couldn\'t be saved.") self.show_message(msg, Qgis.Warning) def import_layer_changed(self, layer): if layer: crs = get_crs_authid(layer.crs()) if crs != DEFAULT_SRS_AUTHID: self.lbl_refactor_source.setStyleSheet('color: orange') self.lbl_refactor_source.setToolTip(QCoreApplication.translate("WizardTranslations", "This layer will be reprojected for you to '{}' (Colombian National Origin),<br>before attempting to import it into LADM-COL.").format( DEFAULT_SRS_AUTHID)) else: self.lbl_refactor_source.setStyleSheet('') self.lbl_refactor_source.setToolTip('') def show_message(self, message, level): self.bar.clearWidgets() # Remove previous messages before showing a new one self.bar.pushMessage(message, level, 10) def save_settings(self): settings = QSettings() point_type = None if self.rad_boundary_point.isChecked(): point_type = 'boundary_point' elif self.rad_survey_point.isChecked(): point_type = 'survey_point' else: point_type = 'control_point' settings.setValue('Asistente-LADM-COL/wizards/points_add_points_type', point_type) settings.setValue('Asistente-LADM-COL/wizards/points_load_data_type', 'csv' if self.rad_csv.isChecked() else 'refactor') settings.setValue('Asistente-LADM-COL/wizards/points_add_points_csv_file', self.txt_file_path.text().strip()) settings.setValue('Asistente-LADM-COL/wizards/points_csv_file_delimiter', self.txt_delimiter.text().strip()) settings.setValue('Asistente-LADM-COL/wizards/points_csv_crs', self.crs) def restore_settings(self): settings = QSettings() point_type = settings.value('Asistente-LADM-COL/wizards/points_add_points_type') or 'boundary_point' if point_type == 'boundary_point': self.rad_boundary_point.setChecked(True) elif point_type == 'survey_point': self.rad_survey_point.setChecked(True) else: # 'control_point' self.rad_control_point.setChecked(True) load_data_type = settings.value('Asistente-LADM-COL/wizards/points_load_data_type') or 'csv' if load_data_type == 'refactor': self.rad_refactor.setChecked(True) else: self.rad_csv.setChecked(True) self.txt_file_path.setText(settings.value('Asistente-LADM-COL/wizards/points_add_points_csv_file')) self.txt_delimiter.setText(settings.value('Asistente-LADM-COL/wizards/points_csv_file_delimiter')) self.crs = settings.value('Asistente-LADM-COL/wizards/points_csv_crs', DEFAULT_SRS_AUTHID, str) self.update_crs_info() def show_help(self): show_plugin_help("create_points")
class RoleRegistry(QObject, metaclass=SingletonQObject): """ Manage all role information. Current role can also be got/set from this class. Roles can set their own GUI configuration, their own LADM-COL supported models, their own quality rules, etc. """ active_role_changed = pyqtSignal(str) # New active role key COMMON_ACTIONS = [ # Common actions for all roles ACTION_LOAD_LAYERS, ACTION_SCHEMA_IMPORT, ACTION_IMPORT_DATA, ACTION_EXPORT_DATA, ACTION_SETTINGS, ACTION_HELP, ACTION_ABOUT ] def __init__(self): QObject.__init__(self) self.logger = Logger() self.app = AppInterface() self._registered_roles = dict() self._default_role = BASIC_ROLE # Register default roles for role_key, role_config in get_role_config().items(): if ROLE_ENABLED in role_config and role_config[ROLE_ENABLED]: self.register_role(role_key, role_config) def register_role(self, role_key, role_dict, activate_role=False): """ Register roles for the LADM-COL assistant. Roles have access only to certain GUI controls, to certain LADM-COL models and to certain quality rules. Warning: this class will modify the role_dict, so better pass a deepcopy of the configuration dict. :param role_key: Role unique identifier :param role_dict: Dictionary with the following information: ROLE_NAME: Name of the role ROLE_DESCRIPTION: Explains what this role is about ROLE_ENABLED: Whether this role is enabled or not ROLE_ACTIONS: List of actions a role has access to ROLE_MODELS: List of models and their configuration for the current role ROLE_QUALITY_RULES: List of quality rule keys this role has access to ROLE_GUI_CONFIG: Dict with the GUI config (menus and toolbars) :return: Whether the role was successfully registered or not. """ valid = False if ROLE_NAME in role_dict and ROLE_DESCRIPTION in role_dict and ROLE_ACTIONS in role_dict and \ ROLE_GUI_CONFIG in role_dict and TEMPLATE_GUI in role_dict[ROLE_GUI_CONFIG] \ and ROLE_MODELS in role_dict: if role_dict[ROLE_GUI_CONFIG]: # It's mandatory to provide a GUI config for the role self._registered_roles[role_key] = role_dict valid = True else: self.logger.error(__name__, "Role '{}' has no GUI config and could not be registered!".format(role_key)) else: self.logger.error(__name__, "Role '{}' is not defined correctly and could not be registered! Check the role_dict parameter.".format(role_key)) if activate_role: self.set_active_role(role_key) return valid def unregister_role(self, role_key): res = False if role_key in self._registered_roles: # You cannot unregister the default role if role_key != self._default_role: # First change active role to default if role_key is active if role_key == self.get_active_role(): self.set_active_role(self._default_role) # Then unregister the role self._registered_roles[role_key] = None del self._registered_roles[role_key] res = False else: self.logger.warning(__name__, "You cannot unregister the default role!") else: self.logger.warning(__name__, "The role ('{}') you're trying to unregister is not registered!".format(role_key)) return res def get_active_role(self): # We make sure the active role we return is in fact registered. # Otherwise, we set the default role as active. active_role = self.app.settings.active_role if not active_role in self._registered_roles: self.set_active_role(self._default_role) return self.app.settings.active_role def get_active_role_name(self): return self.get_role_name(self.get_active_role()) def active_role_already_set(self): """ Whether we have set an active role already or not. :return: True if the current_role_key variable is stored in QSettings. False otherwise. """ return self.app.settings.active_role is not None def set_active_role(self, role_key, emit_signal=True): """ Set the active role for the plugin. :param role_key: Key to identify the role. :param emit_signal: Whether the active_role_changed should be emitted or not. A False argument should be passed if the plugin config refresh will be called manually, for instance, because it is safer to call a GUI refresh after closing some plugin dialogs. :return: Whether the role was successfully changed or not in the role registry. """ res = False if role_key in self._registered_roles: res = True else: self.logger.warning(__name__, "Role '{}' was not found, the default role is now active.".format(role_key)) role_key = self._default_role self.app.settings.active_role = role_key self.logger.info(__name__, "Role '{}' is now active!".format(role_key)) if emit_signal: self.active_role_changed.emit(role_key) return res def set_active_default_role(self, emit_signal=True): return self.set_active_role(self._default_role, emit_signal) def get_roles_info(self): return {k: v[ROLE_NAME] for k,v in self._registered_roles.items()} def get_role_name(self, role_key): if role_key not in self._registered_roles: self.logger.error(__name__, "Role '{}' was not found, returning default role's name".format(role_key)) role_key = self._default_role return self._registered_roles[role_key][ROLE_NAME] def get_role_description(self, role_key): if role_key not in self._registered_roles: self.logger.error(__name__, "Role '{}' was not found, returning default role's decription".format(role_key)) role_key = self._default_role return self._registered_roles[role_key][ROLE_DESCRIPTION] def get_role_actions(self, role_key): if role_key not in self._registered_roles: self.logger.error(__name__, "Role '{}' was not found, returning default role's actions.".format(role_key)) role_key = self._default_role return list(set(self._registered_roles[role_key][ROLE_ACTIONS] + self.COMMON_ACTIONS)) def get_role_gui_config(self, role_key, gui_type=TEMPLATE_GUI): """ Return the role GUI config. :param role_key: Role id. :param gui_type: Either TEMPLATE_GUI or DEFAULT_GUI (the one for wrong db connections). :return: Dict with the GUI config for the role. """ if role_key not in self._registered_roles: self.logger.error(__name__, "Role '{}' was not found, returning default role's GUI configuration.".format(role_key)) role_key = self._default_role # Return a deepcopy, since we don't want external classes to modify a role's GUI config gui_conf = self._registered_roles[role_key][ROLE_GUI_CONFIG].get(gui_type, dict()) return deepcopy(gui_conf) # The plugin knows what to do if the role has no DEFAULT_GUI def get_role_models(self, role_key): """ Normally you wouldn't need this but LADMColModelRegistry, which is anyway updated when the role changes. """ if role_key not in self._registered_roles: self.logger.error(__name__, "Role '{}' was not found, returning default role's models.".format(role_key)) role_key = self._default_role return self._registered_roles[role_key][ROLE_MODELS] def get_active_role_supported_models(self): return self.get_role_supported_models(self.get_active_role()) def get_role_supported_models(self, role_key): return self.get_role_models(role_key)[ROLE_SUPPORTED_MODELS] def active_role_needs_automatic_expression_for_baskets(self): return self._registered_roles[self.get_active_role()].get(ROLE_NEEDS_AUTOMATIC_VALUE_FOR_BASKETS, False) def get_role_quality_rules(self, role_key): if role_key not in self._registered_roles: self.logger.error(__name__, "Role '{}' was not found, returning default role's quality rules.".format(role_key)) role_key = self._default_role return self._registered_roles[role_key][ROLE_QUALITY_RULES] def get_role_db_source(self, role_key): if role_key not in self._registered_roles: self.logger.error(__name__, "Role '{}' was not found, returning default role's db source.".format(role_key)) role_key = self._default_role return self._registered_roles[role_key].get(ROLE_DB_SOURCE, None) def add_actions_to_roles(self, action_keys, role_keys=None): """ For add-ons that want to modify actions of already registered roles. This first adds each action_key to allowed role actions, and then it adds each action key to the menu Add-ons that is empty by default in the template GUI Config. After calling this method, it is necessary to call gui_builder.build_gui() to refresh the GUI with these changes. Otherwise, the user won't see changes until build_gui() is called from the Asistente LADM-COL. :param action_keys: List of action keys. :param role_keys: List of role keys. This param is optional. If it's not passed, we'll use all registered roles. """ if not role_keys: role_keys = list(self._registered_roles.keys()) for role_key in role_keys: if role_key in self._registered_roles: self.__add_actions_to_allowed_role_actions(action_keys, role_key) self.__add_actions_to_role_add_on_menu(action_keys, role_key) self.logger.debug(__name__, "{} actions added to role '{}'!".format(len(action_keys), role_key)) def __add_actions_to_allowed_role_actions(self, action_keys, role_key): # Add action keys to the list of allowed actions for a given role role_actions = self._registered_roles[role_key][ROLE_ACTIONS] self._registered_roles[role_key][ROLE_ACTIONS] = list(set(role_actions + action_keys)) del role_actions def __add_actions_to_role_add_on_menu(self, action_keys, role_key): # Go for the Menu with object_name LADM_COL_ADD_ON_MENU and add the action keys gui_config = self._registered_roles[role_key][ROLE_GUI_CONFIG] for main_menu in gui_config.get(MAIN_MENU, dict()): # Since MAIN_MENU is a list of menus for action in main_menu.get(ACTIONS, list()): if isinstance(action, dict): # We know this is a menu if action.get(OBJECT_NAME, "") == LADM_COL_ADD_ON_MENU: # This is the Add-ons menu action[ACTIONS] = list(set(action[ACTIONS] + action_keys)) # Add actions and avoid dup. break # Go to other menus, because in this one we are done!