class ProfileInstanceRecords(QDialog, FORM_CLASS): """ class constructor The class handles all the instances that the user has collected and saved in the folder and saved in a computer. The class will construct the path to the folder and enumerate all available instances and return the count. It will also rename all the file based on instance unique GUUID for easier management and future updates. """ def __init__(self, parent=None): """ initailize class variables here """ super(ProfileInstanceRecords, self).__init__(parent) self.setupUi(self) self.path = None self.instance_list = [] self.relations = OrderedDict() self.parent_ids = {} self.importlogger = ImportLogger() self._notif_bar_str = NotificationBar(self.vlnotification) self.chk_all.setCheckState(Qt.Checked) self.entity_model = EntitiesModel() self.uuid_extractor = InstanceUUIDExtractor(self.path) self.btn_chang_dir.setIcon(GuiUtils.get_icon("open_file.png")) self.btn_refresh.setIcon(GuiUtils.get_icon("update.png")) self.chk_all.stateChanged.connect(self.change_check_state) # self.cbo_profile.currentIndexChanged.connect(self.current_profile_changed) self.btn_chang_dir.clicked.connect(self.on_directory_search) self.lst_widget.itemClicked.connect(self.user_selected_entities) self.btn_refresh.clicked.connect(self.update_files_with_custom_filter) self.buttonBox.button(QDialogButtonBox.Save).setText('Import') # self.load_config() self.init_file_path() self.current_profile_changed() self.change_check_state(self.chk_all.checkState()) self.instance_dir() def load_config(self): """ Load STDM configuration :return: """ stdm_config = None if QFile.exists(HOME + "/stdm/configuration.stc"): stdm_config = QFile(CONFIG_FILE) ConfigurationFileSerializer(stdm_config) profiles = StdmConfiguration.instance().profiles return profiles def profiles(self): """ Return a list of all profiles :rtype: list """ return list(self.load_config().values()) def change_check_state(self, state): """ Change the check state of items in a list widget """ for i in range(self.lst_widget.count()): self.lst_widget.item(i).setCheckState(state) def current_profile_changed(self): """ Get the current profile so that it is the one selected at the combo box :return: """ self.instance_list = [] self.active_profile() self.init_file_path() self.available_records() self.on_dir_path() self.populate_entities_widget() def active_profile(self): """ get the user selected profile :return:p """ self.profile = current_profile().name return self.profile def instance_dir(self): """ Create a path where imported instance will be kept :return: """ self.inst_path = CONFIG_FILE + "_imported" if not os.access(self.inst_path, os.F_OK): os.makedirs(str(self.inst_path)) else: return self.inst_path def imported_instance_path(self): """ :return: """ self.instance_dir() return self.inst_path def init_file_path(self): """ Initialize GeoODK file path """ self.path = self.geoODK_file_path(self.txt_directory.text()) self.txt_directory.setText(self.path) def geoODK_file_path(self, path=''): """ Check if geoODK file path has been configured, if not configure default and return it. :rtype: string """ if not path.strip(): path = self.make_path(GEOODK_FORM_HOME) return path def make_path(self, path): """ Create and return a file path if is not available. :rtype: string """ if not os.access(path, os.F_OK): os.makedirs(str(path)) return path def xform_xpaths(self): """ Return the full path to the default config path and filter geoodk instance that matches the current profile path :return: directories :rtype: list """ return [ os.path.join(self.path, name) for name in os.listdir(self.path) if os.path.isdir(os.path.join(self.path, name)) if name.startswith(self.profile_formater()) ] def on_dir_path(self): """ Extract the specific folder information and rename the file :return: """ self.uuid_extractor.new_list = [] if self.record_count() > 0: directories = self.xform_xpaths() for directory in directories: self.extract_guuid_and_rename_file(directory) self.importlogger.start_json_file() self.previous_import_instances() # diff = inst_count - rm_count self.txt_count.setText(str(len(self.instance_list))) def extract_guuid_and_rename_file(self, path): """ Extract the unique Guuid and rename the file so that we can uniquely identify each file :return: """ for f in os.listdir(path): if os.path.isfile(os.path.join(path, f)) and f.endswith('.xml'): file_instance = os.path.normcase(os.path.join(path, f)) if os.path.isfile(file_instance): self.rename_file_to_UUID(file_instance) def read_instance_data(self) -> Dict[str, InstanceData]: """Read all instance data once and store them in a dict """ mobile_data = OrderedDict() social_tenure_info = OrderedDict() self.uuid_extractor.unset_path() for instance_p in self.instance_list: if os.path.isfile(instance_p): isntance_dir, instance = os.path.split(instance_p) else: instance = instance_p self.uuid_extractor.set_file_path(instance) mobile_data[instance] = InstanceData( field_data_nodes=self.uuid_extractor. document_entities_with_data( self.active_profile().replace(' ', '_'), self.user_selected_entities()), str_data_nodes=self.uuid_extractor.document_entities_with_data( self.active_profile().replace(' ', '_'), ['social_tenure'])) self.uuid_extractor.close_document() return mobile_data def rename_file_to_UUID(self, file): """ Extract the UUID from each folder and file :return: """ self.uuid_extractor.set_file_path(file) self.uuid_extractor.on_file_passed() self.instance_list = self.uuid_extractor.file_list() def move_imported_file(self, file): """ Moves the imported files to avoid repetition :return: """ instance_path = self.imported_instance_path() try: basename = os.path.basename(os.path.dirname(file)) if not os.path.isdir( os.path.join(self.imported_instance_path(), basename)): shutil.move(os.path.dirname(file), instance_path) except DummyException as ex: return str(ex) def populate_entities_widget(self): """ Add entities in the instance file into a list view widget """ self.lst_widget.clear() entities = self.instance_entities() if len(entities) > 0: for entity in entities: list_widget = QListWidgetItem( current_profile().entity_by_name(entity).short_name, self.lst_widget) list_widget.setCheckState(Qt.Checked) def user_selected_entities(self): """ :rtype: list """ entities = [] count = self.lst_widget.count() if count > 0: for i in range(count): item = self.lst_widget.item(i) if item.checkState() == Qt.Checked: entities.append(current_profile().entity(item.text()).name) return entities def instance_entities(self): """ Enumerate the entities that are in the current profile and also that are captured in the form so that we are only importing relevant entities to database :return: entities """ current_entities = [] entity_collections = [] instance_collections = self.instance_collection() if len(instance_collections) > 0: for entity_name in self.profile_entities_names(current_profile()): if current_profile().entity_by_name(entity_name) is not None: current_entities.append(entity_name) return current_entities def instance_collection(self): """ Enumerate all the instances found in the instance directory rtype: list """ dirs = self.xform_xpaths() instance_collections = [] if len(dirs) > 0: for dir_f in dirs: xml_files = [ dir_f.replace("\\", "/") + '/' + f for f in os.listdir(dir_f) if f.endswith('.xml') ] if len(xml_files) > 0: instance_collections.append(xml_files[0]) return instance_collections def check_profile_with_custom_name(self): """ Try extract mobile instance with custom filter name. Assumption is that there is a profile that bears that name :return: """ mismatch_profile = 'Nothing found to import. \n' \ ' Ensure the current filter text or profile is correct' entity_attr = [] if self.txt_filter.text() != '': for obj in self.profiles(): if obj.name.startswith(self.txt_filter.text()): if obj.name != current_profile().name: self._notif_bar_str.insertErrorNotification( mismatch_profile) return return self.uuid_extractor.document_entities(self.profile) def profile_entities_names(self, profile): """ Return names of all entities in a profile :rtype: list """ entities_names = [] for entity in profile.user_entities(): entities_names.append(entity.name) return entities_names def has_foreign_keys_parent(self, select_entities): """ Ensure we check that the table is not parent else import parent table first Revised in version 1.7. It explicitly assumes str is captured. before it was optional. :return: """ has_relations = False str_tables = current_profile().social_tenure party_tbls = str_tables.parties sp_tbls = str_tables.spatial_units self.relations = OrderedDict() if len(self.instance_list) > 0: if self.uuid_extractor.has_str_captured_in_instance( self.instance_list[0]): for party_tbl in party_tbls: self.relations[party_tbl.name] = [ 'social_tenure_relationship', party_tbl.short_name.lower() + '_id' ] for sp_tbl in sp_tbls: self.relations[sp_tbl.name] = [ 'social_tenure_relationship', sp_tbl.short_name.lower() + '_id' ] for table in select_entities: table_object = current_profile().entity_by_name(table) cols = list(table_object.columns.values()) for col in cols: if col.TYPE_INFO == 'FOREIGN_KEY': parent_object = table_object.columns[col.name] if parent_object.parent: if parent_object.parent.name in self.relations: self.relations[parent_object.parent.name].append( [table, col.name]) else: self.relations[parent_object.parent.name] = [ table, col.name ] # self.relations[parent_object.parent.name].append([table, col.name]) has_relations = True else: continue return has_relations def parent_table_isselected(self): """ Take note that the user selected tables may or may not be imported based on parent child table relationship. Add those table silently so that we can show them to the user :return: """ try: silent_list = [] entities = self.user_selected_entities() if len(entities) > 0: for table in self.relations.keys(): if table not in entities: silent_list.append(table) return silent_list except DummyException as ex: self._notif_bar_str.insertErrorNotification(str(ex)) def archive_this_import_file(self, counter, instance): """ Ensure that only import are done once :return: """ try: self.importlogger.logger_sections() file_info = 'File instance ' + str(counter) + ' : \n' + instance ImportLogger.log_action(file_info) except IOError as io: self._notif_bar_str.insertErrorNotification(MSG + ": " + str(io)) pass def log_table_entry(self, instance): """ Ensure that only import are done once :return: """ try: current_time = QDateTime() import_time = current_time.currentDateTime() log_entry = instance + ' ' + str(import_time.toPyDateTime()) ImportLogger.log_action(log_entry) except IOError as io: self._notif_bar_str.insertErrorNotification(MSG + ": " + str(io)) raise NameError(str(io)) def check_previous_import(self): """ Ensure we are importing files once :return: """ try: self.importlogger.add_log_info() for files in self.instance_list: current_dir = os.path.basename(files) exist = self.importlogger.check_file_exist(current_dir) if exist: self.instance_list.remove(files) self.txt_count.setText(str(len(self.instance_list))) if self.record_count() != len(self.instance_list): msg = 'Some files have been already imported and therefore ' \ 'not enumerated' self._notif_bar_str.insertErrorNotification(msg) except IOError as io: self._notif_bar_str.insertErrorNotification(MSG + ": " + str(io)) pass def available_records(self): """ Let the user know how many records have been collected and are available for inport process :return: """ self.txt_count.setText(str(self.record_count())) def record_count(self): """ get the count of instance dir in the selected directory :return: integer """ return len([ name for name in os.listdir(self.path) if os.path.isdir(os.path.join(self.path, name)) if name.startswith(self.profile_formater()) ]) def profile_formater(self): """ Format the profile name by removing underscore character :return: """ if self.txt_filter.text() != '': filter_text = self.txt_filter.text() return filter_text else: return self.profile def update_files_with_custom_filter(self): """ Get the new file count with the user custom filter text :return: file count """ self.available_records() self.on_dir_path() self.populate_entities_widget() self.buttonBox.button(QDialogButtonBox.Save).setEnabled(True) def projection_settings(self): """ let user select the projections for the data :return: """ project_select = ProjectionSelector(self) projection = project_select.loadAvailableSystems() self.txt_srid.setText(str(projection)) def on_directory_search(self): """ Let the user choose the directory with instances :return: """ home_path = 'home' if self.txt_directory.text() != '': home_path = self.txt_directory.text() dir_name = QFileDialog.getExistingDirectory(self, 'Open Directory', home_path, QFileDialog.ShowDirsOnly) if dir_name: self.txt_directory.setText(str(dir_name)) self.current_profile_changed() self.change_check_state(self.chk_all.checkState()) def feedback_message(self, msg): """ Create a dialog box to capture and display errrors related to db while importing data :param: msg :type: string :return:Qdialog """ msgbox = QMessageBox() msgbox.setStandardButtons(QMessageBox.Ok | QMessageBox.No) msgbox.setWindowTitle("Data Import") msgbox.setText(msg) msgbox.exec_() msgbox.show() return msgbox def save_instance_data_to_db(self, entities): """ Get the user selected entities and insert them into database params: selected entities rtype: list :return:Object :type: dbObject """ cu_obj = '' import_status = False self.txt_feedback.clear() self.txt_feedback.append("Import started, please wait...\n") QCoreApplication.processEvents() self._notif_bar_str.clear() mobile_field_data = self.read_instance_data() self.has_foreign_keys_parent(entities) if len(self.parent_table_isselected()) > 0: if QMessageBox.information( self, QApplication.translate('GeoODKMobileSettings', " Import Warning"), QApplication.translate( 'GeoODKMobileSettings', 'Some of dependent tables (entities)' 'which may not be part of the selected tables ' 'I.e: {} will be imported'.format( self.parent_table_isselected())), QMessageBox.Ok | QMessageBox.No) == QMessageBox.No: return if not mobile_field_data: self.feedback_message('Not matching data in mobile files') return counter = 0 try: self.pgbar.setRange(counter, len(self.instance_list)) self.pgbar.setValue(0) ImportLogger.log_action("Import started ...\n") for instance_obj, instance_obj_data in mobile_field_data.items(): ImportLogger.log_action("File {} ...\n".format(instance_obj)) parents_info = [] import_status = False counter = counter + 1 self.parent_ids = {} single_occuring, repeated_entities = self.uuid_extractor.attribute_data_from_nodelist( instance_obj_data.field_data_nodes) single_occurring_keys = list(single_occuring.keys()) for entity in single_occurring_keys: entity_data = single_occuring[entity] import_status = False if entity in self.relations: #if entity not in self.parent_ids.keys(): self.count_import_file_step(counter, entity) log_timestamp = '=== parent table import === : {0}'.format( entity) cu_obj = entity self.log_table_entry(log_timestamp) entity_add = Save2DB(entity, entity_data, self.parent_ids) entity_add.objects_from_supporting_doc(instance_obj) ref_id = entity_add.save_parent_to_db() import_status = True self.parent_ids[entity] = [ref_id, entity] parents_info.append(entity) single_occuring.pop(entity) elif entity not in self.relations: import_status = False self.count_import_file_step(counter, entity) log_timestamp = '=== standalone table import === : {0}'.format( entity) cu_obj = entity self.log_table_entry(log_timestamp) entity_add = Save2DB(entity, entity_data, self.parent_ids) entity_add.objects_from_supporting_doc(instance_obj) child_id = entity_add.save_to_db() cu_obj = entity import_status = True parents_info.append(entity) if entity not in self.parent_ids.keys(): self.parent_ids[entity] = [child_id, entity] entity_add.cleanup() if repeated_entities: # self.log_table_entry(" ========== starting import of repeated tables ============") import_status = False for repeated_entity, entity_data in repeated_entities.items( ): """We are assuming that the number of repeat table cannot exceed 99""" enum_index = repeated_entity[:2] if enum_index.isdigit(): repeat_table = repeated_entity[2:] else: enum_index = repeated_entity[:1] repeat_table = repeated_entity[1:] log_timestamp = ' child table {0} >> : {1}' \ .format(repeat_table, enum_index) self.count_import_file_step(counter, repeat_table) ImportLogger.log_action(log_timestamp) if repeat_table in self.profile_entities_names( current_profile()): entity_add = Save2DB(repeat_table, entity_data, self.parent_ids) entity_add.objects_from_supporting_doc( instance_obj) child_id = entity_add.save_to_db() self.parent_ids[repeat_table] = [ child_id, repeat_table ] cu_obj = repeat_table import_status = True self.log_table_entry( " ------ import succeeded: {0} ".format( import_status)) entity_add.cleanup() QCoreApplication.processEvents() else: continue if instance_obj_data.str_data_nodes: '''We treat social tenure entities separately because of foreign key references''' entity_relation = EntityImporter(instance_obj) single_str, multiple_str = self.uuid_extractor.attribute_data_from_nodelist( instance_obj_data.str_data_nodes) if len(single_str) > 0: entity_relation.process_social_tenure( single_str, self.parent_ids) elif len(multiple_str) > 1: for repeated_entity, entity_data in multiple_str.items( ): """We are assuming that the number of repeat str cannot exceed 10""" entity_relation.process_social_tenure( entity_data, self.parent_ids) self.log_table_entry( " ----- saving social tenure relationship") # entity_add.cleanup() self.txt_feedback.append( 'saving record "{0}" to database'.format(counter)) self.pgbar.setValue(counter) self.log_instance(instance_obj) QCoreApplication.processEvents() self.txt_feedback.append( 'Number of records successfully imported: {}'.format(counter)) except DummyException as ex: self.feedback_message(str(ex)) QCoreApplication.processEvents() QApplication.restoreOverrideCursor() self.buttonBox.setEnabled(True) except SQLAlchemyError as ae: QCoreApplication.processEvents() QApplication.restoreOverrideCursor() self.feedback_message(str(ae)) self.txt_feedback.append( "current table {0}import failed...\n".format(cu_obj)) self.txt_feedback.append(str(ae)) self.log_table_entry(str(ae)) return def count_import_file_step(self, count=None, table=None): """ Tracking method to record the current import activity :param count: int :param table: string :return: """ self.txt_feedback.append(' Table : {}'.format(table)) def accept(self): """ Execute the import dialog once the save button has been clicked :return: """ self.buttonBox.setEnabled(False) QApplication.setOverrideCursor(Qt.WaitCursor) try: if self.lst_widget.count() < 1: msg = 'No mobile records found for the current profile' self._notif_bar_str.insertErrorNotification(msg) self.buttonBox.setEnabled(True) QApplication.restoreOverrideCursor() return entities = self.user_selected_entities() if len(entities) < 1: if QMessageBox.information( self, QApplication.translate('MobileForms', 'Import Warning'), QApplication.translate( 'MobileForms', 'You have not ' 'selected any entity for import. All entities ' 'will be imported'), QMessageBox.Ok | QMessageBox.No) == QMessageBox.Ok: entities = self.instance_entities() QApplication.restoreOverrideCursor() else: self.buttonBox.setEnabled(True) QApplication.restoreOverrideCursor() return else: self.save_instance_data_to_db(entities) self.buttonBox.setEnabled(True) QApplication.restoreOverrideCursor() except DummyException as ex: QApplication.restoreOverrideCursor() self.feedback_message(str(ex)) self.log_table_entry(str(ex)) self.buttonBox.setEnabled(True) self.buttonBox.button(QDialogButtonBox.Save).setEnabled(True) QApplication.restoreOverrideCursor() return def log_instance(self, instance): # instance_short_name = self.importlogger.log_data_name(instance) log_data = self.importlogger.read_log_data() log_data[instance] = self.importlogger.log_date() self.importlogger.write_log_data(log_data) def previous_import_instances(self): count = 0 del_list = [] log_data = self.importlogger.read_log_data() if len(log_data) > 0: for instance in self.instance_list: # dir_path, file_name = os.path.split(instance) # if log_data.has_key(instance) or log_data.has_key(file_name): try: if os.path.split(instance)[1] in log_data: # del_list. append(instance) self.instance_list.remove(instance) else: continue except DummyException: continue # if len(del_list)>0: # [self.instance_list.remove(inst) for inst in del_list] def close(self): """ when the user interrupts data import operations, we should close exit """ self.instance_list = None QApplication.restoreOverrideCursor() QCoreApplication.processEvents() self.close()
class ValueEditor(QDialog, Ui_LookupValue): """ Form to add/edit values added to a lookup. Values are objects of type CodeValue """ def __init__(self, parent, lookup, code_value=None): """ :param parent: Owner of this dialog window :type parent: QWidget :param lookup: A value list object to add the value :type lookup: ValueList :param code_value: A value object to add to the lookup, if None this is a new value, else its an edit. :type code_value: CodeValue """ QDialog.__init__(self, parent) self.setupUi(self) self.lookup = lookup self.code_value = code_value self.notice_bar = NotificationBar(self.notif_bar) self.init_gui() def init_gui(self): """ initializes the form widgets """ # Set character length constraints self.edtValue.setMaxLength(50) self.edtCode.setMaxLength(5) if self.code_value: if self.code_value.updated_value == '': self.edtValue.setText(self.code_value.value) self.edtCode.setText(self.code_value.code) else: self.edtValue.setText(self.code_value.updated_value) self.edtCode.setText(self.code_value.updated_code) self.edtCode.textChanged.connect(self.validate_text) self.edtValue.textChanged.connect(self.validate_text) self.edtValue.setFocus() def show_notification(self, message): self.notice_bar.clear() self.notice_bar.insertErrorNotification(message) def validate_text(self, text): """ Validates and updates the entered text if necessary. :param text: The text entered :type text: String """ text_edit = self.sender() cursor_position = text_edit.cursorPosition() text_edit.setValidator(None) if len(text) == 0: return locale = QSettings().value("locale/userLocale")[0:2] if locale == 'en': name_regex = QtCore.QRegExp('^[ _0-9a-zA-Z][a-zA-Z0-9_/\\-()|.:,; ]*$') name_validator = QtGui.QRegExpValidator(name_regex) text_edit.setValidator(name_validator) QApplication.processEvents() last_character = text[-1:] state = name_validator.validate(text, text.index(last_character))[0] if state != QValidator.Acceptable: self.show_notification(u'"{}" is not allowed at this position.'. format(last_character) ) text = text[:-1] if len(text) > 1: if text[0] == ' ' or text[0] == '_': text = text[1:] self.blockSignals(True) text_edit.setText(text) text_edit.setCursorPosition(cursor_position) self.blockSignals(False) text_edit.setValidator(None) def add_value(self): """ Adds a code value to a lookup object. Checks first if a previous value exist then removes it and then adds the new one. """ value = unicode(self.edtValue.text().strip()) code = unicode(self.edtCode.text().strip()) # if its an edit, first remove the previous value if self.code_value: self.lookup.rename(self.code_value.value, value, code) else: self.lookup.add_code_value(CodeValue(code, value)) def accept(self): if self.edtValue.text() == '' or self.edtValue.text() == ' ': self.error_message(QApplication.translate( "ValueEditor", "Please enter a valid lookup value.") ) return self.add_value() self.done(1) def reject(self): self.done(0) def error_message(self, message): """ Creates a message box and displays a message :param message: message to display :type message: str """ msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("STDM") msg.setText(message) msg.exec_()
class LookupDialog(QDialog, Ui_LookupTranslatorDialog, TranslatorDialogBase): """ Dialog for defining configuration settings for the lookup translation implementation. """ def __init__(self, parent, source_cols, dest_table, dest_col, src_col): QDialog.__init__(self, parent) self.setupUi(self) TranslatorDialogBase.__init__(self, source_cols, dest_table, dest_col, src_col) self._notif_bar = NotificationBar(self.vl_notification) # Populate controls self._load_lookup_tables() # Connect signals self.cbo_lookup.currentIndexChanged.connect( self._on_lookup_table_name_changed) def _load_lookup_tables(self): # Load lookup table names c_profile = self._current_profile if c_profile is None: msg = QApplication.translate( 'LookupDialog', 'Current profile could not be determined.') self._notif_bar.clear() self._notif_bar.insertErrorNotification(msg) return lookups = [e.name for e in c_profile.value_lists()] self.cbo_lookup.clear() self.cbo_lookup.addItem('') self.cbo_lookup.addItems(lookups) def _on_lookup_table_name_changed(self, idx): # Slot raised when the lookup table name changes. self.cbo_default.clear() if idx == -1: return t_name = self.cbo_lookup.currentText() self.load_lookup_values(t_name) def load_lookup_values(self, table_name): """ Load the default value combobox with values from the specified lookup table name. :param table_name: Lookup table name. :type table_name: str """ self.cbo_default.clear() if not table_name: return # Get lookup entity lk_ent = self._current_profile.entity_by_name(table_name) if lk_ent is None: msg = QApplication.translate('LookupDialog', 'Lookup values could not be loaded.') self._notif_bar.clear() self._notif_bar.insertErrorNotification(msg) return lk_values = lk_ent.lookups() self.cbo_default.addItem('') for lk_value in lk_values: text_value = None for k, v in lk_ent.values.items(): if v.value == lk_value: text_value = v.value if text_value is not None: self.cbo_default.addItem(text_value) def value_translator(self): """ :return: Returns the lookup value translator object. :rtype: LookupValueTranslator """ lookup_translator = LookupValueTranslator() lookup_translator.set_referencing_table(self._dest_table) lookup_translator.set_referencing_column(self._dest_col) lookup_translator.set_referenced_table(self.cbo_lookup.currentText()) lookup_translator.add_source_reference_column(self._src_col, self._dest_col) lookup_translator.default_value = self.cbo_default.currentText() return lookup_translator def validate(self): """ Check user configuration and validate if they are correct. :return: Returns True if user configuration is correct, otherwise False. :rtype: bool """ if not self.cbo_lookup.currentText(): msg = QApplication.translate( 'LookupDialog', 'Please select the referenced lookup table.') self._notif_bar.clear() self._notif_bar.insertWarningNotification(msg) return False return True def accept(self): """ Validate before accepting user input. """ if self.validate(): super(LookupDialog, self).accept()
class EntityEditor(QDialog, Ui_dlgEntity): """ Dialog to add and edit entities """ def __init__(self, **kwargs): """ :param parent: Owner of this dialog :param profile : current profile :param entity : current entity :param in_db : Boolean flag to check if entity exist in database """ self.form_parent = kwargs.get('parent', self) self.profile = kwargs.get('profile', None) self.entity = kwargs.get('entity', None) self.in_db = kwargs.get('in_db', False) QDialog.__init__(self, self.form_parent) self.setupUi(self) self.notice_bar = NotificationBar(self.notif_bar) self.init_gui_controls() def init_gui_controls(self): self.edtTable.setFocus() self.setTabOrder(self.edtTable, self.edtDesc) if self.entity: self.edtTable.setText(self.entity.short_name) self.edtDesc.setText(self.entity.description) self.cbSupportDoc.setCheckState( self.bool_to_check(self.entity.supports_documents)) if self.entity.supports_documents and self.supporting_document_exists( ): self.cbSupportDoc.setEnabled(False) self.edtTable.textChanged.connect(self.validate_text) self.edtTable.setEnabled(not self.in_db) def supporting_document_exists(self): sd_name = u'{0}_{1}_{2}'.format(self.profile.prefix, self.entity.short_name.lower(), 'supporting_document') return pg_table_exists(sd_name) def show_notification(self, message): """ Shows a warning notification bar message. :param message: The message of the notification. :type message: String """ msg = self.tr(message) self.notice_bar.clear() self.notice_bar.insertErrorNotification(msg) def validate_text(self, text): """ Validates and updates the entered text if necessary. Spaces are replaced by _ and capital letters are replaced by small. :param text: The text entered :type text: String """ text_edit = self.sender() cursor_position = text_edit.cursorPosition() text_edit.setValidator(None) if len(text) == 0: return name_regex = QRegExp('^(?=.{0,40}$)[ _a-zA-Z][a-zA-Z0-9_ ]*$') name_validator = QRegExpValidator(name_regex) text_edit.setValidator(name_validator) QApplication.processEvents() last_character = text[-1:] state = name_validator.validate(text, text.index(last_character))[0] if state != QValidator.Acceptable: self.show_notification( '"{}" is not allowed at this position.'.format(last_character)) text = text[:-1] else: # remove space and underscore at the beginning of the text if len(text) > 1: if text[0] == ' ' or text[0] == '_': text = text[1:] self.blockSignals(True) text_edit.setText(text) text_edit.setCursorPosition(cursor_position) self.blockSignals(False) text_edit.setValidator(None) def bool_to_check(self, state): """ Returns a check state given a boolean value :param state : Boolean value :type state: Boolean """ if state: return Qt.Checked else: return Qt.Unchecked def accept(self): if self.edtTable.text() == '' or self.edtTable.text() == ' ': self.show_message(self.tr("Please enter a valid entity name.")) return sn = unicode(self.edtTable.text().strip()) short_name = sn[0].upper() + sn[1:] if self.entity is None: # New entity if self.duplicate_check(short_name): self.show_message( self.tr("Entity with the same name already " "exist in the current profile!")) return else: self.add_entity(short_name) self.done(0) else: self.edit_entity(short_name) self.done(1) def add_entity(self, short_name): """ Creates and adds a new entity to a profile :param entity_name: name of the new entity :type entity_name: str """ self.entity = self._create_entity(short_name) self.profile.add_entity(self.entity) #return True def _create_entity(self, short_name): entity = self.profile.create_entity( short_name, entity_factory, supports_documents=self.support_doc()) entity.description = self.edtDesc.text() entity.column_added.connect(self.form_parent.add_column_item) entity.column_removed.connect(self.form_parent.delete_column_item) return entity def edit_entity(self, short_name): # remove old entity #self.profile.remove_entity(self.entity.short_name) #self.entity = self._create_entity(short_name) old_short_name = self.entity.short_name if old_short_name <> short_name: status = self.profile.rename(old_short_name, short_name) self.entity.short_name = short_name self.entity.description = self.edtDesc.text() self.entity.supports_documents = self.support_doc() def duplicate_check(self, name): """ Return True if we have an entity in the current profile with same 'name' :param name: entity short_name :type name: str """ return self.profile.entities.has_key(name) def support_doc(self): """ Return boolean value representing the check state of supporting document checkbox """ values = [False, None, True] cs = values[self.cbSupportDoc.checkState()] return cs def format_internal_name(self, short_name): """ Returns a table name used internally by the entity :param short_name: Entity name entered by user :type short_name: str :rtype: str """ name = unicode(short_name).strip() name = name.replace(' ', "_") name = name.lower() #Ensure prefix is not duplicated in the names prfx = self.profile.prefix prefix_idx = name.find(prfx, 0, len(prfx)) #If there is no prefix then append if prefix_idx == -1: name = u'{0}_{1}'.format(self.profile.prefix, name) return name def reject(self): self.done(0) def show_message(self, message): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("STDM") msg.setText(message) msg.exec_()
class loginDlg(WIDGET, BASE): ''' This class handles user authentication for accessing STDM resources ''' def __init__(self, parent=None, test_connect_mode=False): QDialog.__init__(self, parent) self.setupUi(self) QgsGui.enableAutoGeometryRestore(self) self.btn_db_settings.setIcon( GuiUtils.get_icon('db_server_settings.png')) # If dialog is used in the context of testing database connections self._test_connect_mode = test_connect_mode # gui initialization self.initGui() # class properties self.user = None self.dbConn = None def initGui(self): ''' Initialize GUI ''' # Change the name of the OK button to Login btnLogin = self.btnBox.button(QDialogButtonBox.Ok) if self._test_connect_mode: btnLogin.setText(QApplication.translate("LoginDialog", "Test")) self.setWindowTitle( QApplication.translate("LoginDialog", "STDM Database " "Connection")) self.btn_db_settings.setVisible(False) else: btnLogin.setText(QApplication.translate("LoginDialog", "Login")) self.btn_db_settings.setVisible(True) # Connect slots self.btn_db_settings.clicked.connect(self.settingsDialog) self.btnBox.accepted.connect(self.acceptdlg) # Configure notification bar self.notifBar = NotificationBar(self.vlNotification) if self._test_connect_mode: self.txtUserName.setFocus() else: self.txtUserName.setText('postgres') self.txtPassword.setFocus() def test_connect_mode(self): return self._test_connect_mode def set_database_connection(self, db_connection): """ Set database connection object. User object will be overwritten as it will be derived from the username and password controls. :param db_connection: Database connection object :type db_connection: DatabaseConnection """ self.dbConn = db_connection def validateInput(self): ''' Assert whether required fields have been entered ''' self.notifBar.clear() if self.txtUserName.text() == "": self.notifBar.insertErrorNotification( QApplication.translate("loginDlg", "UserName field cannot be empty")) self.txtUserName.setFocus() return False if self.txtPassword.text() == "": self.notifBar.insertErrorNotification( QApplication.translate("loginDlg", "Password field cannot be empty")) self.txtPassword.setFocus() return False else: return True def _set_user(self): ''' Create the user object based on the user input ''' username = self.txtUserName.text() password = self.txtPassword.text() self.user = User(username, password) def settingsDialog(self): ''' In case the user clicks reset button to change the settings ''' setting_data = self.reg_setting() dbDlg = dbconnDlg(self) if 'Database' in setting_data.keys(): dbDlg.txtDatabase.setText(str(setting_data['Database'])) if 'Host' in setting_data.keys(): dbDlg.txtHost.setText(str(setting_data['Host'])) if 'Port' in setting_data.keys(): dbDlg.txtPort.setText(str(setting_data['Port'])) dbDlg.exec_() def reg_setting(self): connSettings = ['Host', 'Database', 'Port'] set_conn = RegistryConfig() settings = set_conn.read(connSettings) return settings def acceptdlg(self): ''' On user clicking the login button ''' isValid = self.validateInput() if isValid: # Set user object self._set_user() # Get mode and corresponding database connection object if not self._test_connect_mode: # Get DB connection dbconfig = DatabaseConfig() dbconn = dbconfig.read() if dbconn is None: msg = QApplication.translate( "loginDlg", "The STDM database " "connection has not " "been configured in " "your system.\nWould " "you like to configure " "it now?") response = QMessageBox.warning( self, QApplication.translate("LoginDialog", "Database Connection"), msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if response == QMessageBox.Yes: dbDlg = dbconnDlg(self) if dbDlg.exec_() == QDialog.Accepted: # set the partial database connection properties dbconn = dbDlg.dbconn else: dbconn = self.dbConn # Whatever the outcome of the database settings definition process if dbconn is None: return dbconn.User = self.user # Test connection success, msg = dbconn.validateConnection() if success: self.dbConn = dbconn self.accept() else: QMessageBox.critical( self, QApplication.translate("LoginDialog", "Authentication Failed"), msg) self.txtPassword.setFocus() self.txtPassword.selectAll()
class ValueEditor(WIDGET, BASE): """ Form to add/edit values added to a lookup. Values are objects of type CodeValue """ def __init__(self, parent, lookup, code_value=None): """ :param parent: Owner of this dialog window :type parent: QWidget :param lookup: A value list object to add the value :type lookup: ValueList :param code_value: A value object to add to the lookup, if None this is a new value, else its an edit. :type code_value: CodeValue """ QDialog.__init__(self, parent) self.setupUi(self) self.lookup = lookup self.code_value = code_value self.notice_bar = NotificationBar(self.notif_bar) self.init_gui() def init_gui(self): """ initializes the form widgets """ # Set character length constraints self.edtValue.setMaxLength(50) self.edtCode.setMaxLength(5) if self.code_value: if self.code_value.updated_value == '': self.edtValue.setText(self.code_value.value) self.edtCode.setText(self.code_value.code) else: self.edtValue.setText(self.code_value.updated_value) self.edtCode.setText(self.code_value.updated_code) self.edtCode.textChanged.connect(self.validate_text) self.edtValue.textChanged.connect(self.validate_text) self.edtValue.setFocus() def show_notification(self, message): self.notice_bar.clear() self.notice_bar.insertErrorNotification(message) def validate_text(self, text): """ Validates and updates the entered text if necessary. :param text: The text entered :type text: String """ text_edit = self.sender() cursor_position = text_edit.cursorPosition() text_edit.setValidator(None) if len(text) == 0: return locale = (QSettings().value("locale/userLocale") or 'en-US')[0:2] if locale == 'en': name_regex = QRegExp('^[ _0-9a-zA-Z][a-zA-Z0-9_/\\-()|.:,; ]*$') name_validator = QRegExpValidator(name_regex) text_edit.setValidator(name_validator) QApplication.processEvents() last_character = text[-1:] state = name_validator.validate(text, text.index(last_character))[0] if state != QValidator.Acceptable: self.show_notification('"{}" is not allowed at this position.'. format(last_character) ) text = text[:-1] if len(text) > 1: if text[0] == ' ' or text[0] == '_': text = text[1:] self.blockSignals(True) text_edit.setText(text) text_edit.setCursorPosition(cursor_position) self.blockSignals(False) text_edit.setValidator(None) def add_value(self): """ Adds a code value to a lookup object. Checks first if a previous value exist then removes it and then adds the new one. """ value = str(self.edtValue.text().strip()) code = str(self.edtCode.text().strip()) # if its an edit, first remove the previous value if self.code_value: self.lookup.rename(self.code_value.value, value, code) else: self.lookup.add_code_value(CodeValue(code, value)) def accept(self): if self.edtValue.text() == '' or self.edtValue.text() == ' ': self.error_message(QApplication.translate( "ValueEditor", "Please enter a valid lookup value.") ) return self.add_value() self.done(1) def reject(self): self.done(0) def error_message(self, message): """ Creates a message box and displays a message :param message: message to display :type message: str """ msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("STDM") msg.setText(message) msg.exec_()
class ProfileInstanceRecords(QDialog, FORM_CLASS): """ class constructor The class handles all the instances that the user has collected and saved in the folder and saved in a computer. The class will construct the path to the folder and enumerate all available instances and return the count. It will also rename all the file based on instance unique GUUID for easier management and future updates. """ def __init__(self, parent=None): """ initailize class variables here """ super(ProfileInstanceRecords, self).__init__(parent) self.setupUi(self) self.path = None self.instance_list = [] self.relations = {} self.parent_ids = {} self.importlogger = ImportLogger() self._notif_bar_str = NotificationBar(self.vlnotification) self.chk_all.setCheckState(Qt.Checked) self.entity_model = EntitiesModel() self.uuid_extractor = InstanceUUIDExtractor(self.path) self.btn_chang_dir.setIcon( QIcon(":/plugins/stdm/images/icons/open_file.png")) self.btn_refresh.setIcon( QIcon(":/plugins/stdm/images/icons/update.png")) self.btn_srid.setIcon(QIcon(":/plugins/stdm/images/icons/edit24.png")) self.chk_all.stateChanged.connect(self.check_state_on) #self.cbo_profile.currentIndexChanged.connect(self.current_profile_changed) self.btn_chang_dir.clicked.connect(self.on_directory_search) self.lst_widget.itemClicked.connect(self.user_selected_entities) self.btn_srid.clicked.connect(self.projection_settings) self.btn_refresh.clicked.connect(self.update_files_with_custom_filter) #self.load_config() self.on_filepath() self.current_profile_changed() self.check_state_on() self.instance_dir() def load_config(self): """ Load STDM configuration :return: """ stdm_config = None if QFile.exists(HOME + "/stdm/configuration.stc"): stdm_config = QFile(CONFIG_FILE) ConfigurationFileSerializer(stdm_config) profiles = StdmConfiguration.instance().profiles return profiles def check_state_on(self): """ Ensure all the items in the list are checked :return: """ count = self.lst_widget.count() if count > 0: for i in range(count): item = self.lst_widget.item(i) if self.chk_all.isChecked(): item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) def profiles(self): """ Get all profiles :return: """ return self.load_config().values() def current_profile_changed(self): """ Get the current profile so that it is the one selected at the combo box :return: """ self.instance_list = [] self.active_profile() self.on_filepath() self.available_records() self.on_dir_path() self.profile_instance_entities() def active_profile(self): """ get the user selected profile :return:p """ self.profile = current_profile().name return self.profile def instance_dir(self): """ Create a path where imported instance will be kept :return: """ self.inst_path = self.path + "imported_instance" if not os.access(self.inst_path, os.F_OK): os.makedirs(unicode(self.inst_path)) else: return self.inst_path def instance_path(self): """ :return: """ self.instance_dir() return self.inst_path def on_filepath(self): """ Access the file directory with geoodk files by constructing the full path :return: path :rtype: string """ if self.txt_directory.text() != '': self.path = self.txt_directory.text() else: self.path = GEOODK_FORM_HOME if not os.access(self.path, os.F_OK): os.makedirs(unicode(self.path)) self.txt_directory.setText(self.path) return self.path def xform_xpaths(self): """ Return the full path to the default config path and filter geoodk instance that matches the current profile path :return: directories :rtype: list """ dirs = [] return [ os.path.join(self.path, name) for name in os.listdir(self.path) if os.path.isdir(os.path.join(self.path, name)) if name.startswith(self.profile_formater()) ] def on_dir_path(self): """ Extract the specific folder information and rename the file :return: """ self.uuid_extractor.new_list = [] if self.record_count() > 0: directories = self.xform_xpaths() for directory in directories: self.extract_guuid_and_rename_file(directory) def extract_guuid_and_rename_file(self, path): """ Extract teh unique Guuid and rename the file so that we can uniquely identify each file :return: """ for f in os.listdir(path): if os.path.isfile(os.path.join(path, f)) and f.endswith('.xml'): file_instance = os.path.join(path, f) self.rename_file_to_UUID(file_instance) def rename_file_to_UUID(self, file): """ Extract the UUID from each folder and file :return: """ self.uuid_extractor.set_file_path(file) self.uuid_extractor.on_file_passed() self.instance_list = self.uuid_extractor.new_list def move_imported_file(self, file): """ Moves the imported files to avoid repetition :return: """ instance_path = self.instance_path() try: basename = os.path.basename(os.path.dirname(file)) if not os.path.isdir(os.path.join(self.instance_path(), basename)): shutil.move(os.path.dirname(file), instance_path) else: pass except Exception as ex: return ex def profile_instance_entities(self): """ Add the user entities that are in the form to be imported into database into a list view widget :return: model """ self.lst_widget.clear() entity_list = self.instance_entities() if entity_list is not None and len(entity_list) > 0: for entity in entity_list: list_widget = QListWidgetItem( current_profile().entity_by_name(entity).short_name, self.lst_widget) list_widget.setCheckState(Qt.Checked) else: return def user_selected_entities(self): """ :return: """ user_list = [] count = self.lst_widget.count() if count > 0: for i in range(count): item = self.lst_widget.item(i) if item.checkState() == Qt.Checked: user_list.append(current_profile().entity( item.text()).name) return user_list else: return None def instance_entities(self): """ Enumerate the entities that are in the current profile and also part of the form so that we are only importing relevant entities to database :return: entities """ dirs = self.xform_xpaths() current_etities = [] if len(dirs) > 0: dir_f = dirs[0] instance_file = [ f for f in os.listdir(dir_f) if f.endswith('.xml') ] if len(instance_file) > 0: self.uuid_extractor.set_file_path( os.path.join(dir_f, instance_file[0])) entity_list = self.check_profile_with_custom_name() for entity_name in entity_list: if current_profile().entity_by_name( entity_name) is not None: current_etities.append(entity_name) if len(current_etities) > 0: return current_etities def check_profile_with_custom_name(self): """ Try extract mobile instance with custom filter name. Assumption is that there is a profile that bears that name :return: """ mismatch_profile = 'Please set current profile based on the data to be imported' entity_attr = [] if self.txt_filter.text() != '': for obj in self.profiles(): if obj.name.startswith(self.txt_filter.text()): if obj.name != current_profile().name: self._notif_bar_str.insertErrorNotification( mismatch_profile) return return self.uuid_extractor.document_entities(self.profile) def entity_attribute_to_database(self, entity_info): """ Get the user selected entities and insert tehm into database params: selected entities rtype: list :return:Object :type: dbObject """ cu_obj = '' import_status = False self.txt_feedback.clear() self._notif_bar_str.clear() has_relations = self.has_foreign_keys_parent(entity_info) if len(self.parent_table_isselected()) > 0: if QMessageBox.information( self, QApplication.translate('GeoODKMobileSettings', " Import Warning"), QApplication.translate( 'GeoODKMobileSettings', 'Some of dependent tables (entities)' 'which may not be part of the selected tables ' 'I.e: {} will be imported'.format( self.parent_table_isselected())), QMessageBox.Ok | QMessageBox.No) == QMessageBox.No: return try: parents_info = [] counter = 0 if len(self.instance_list) > 0: self.pgbar.setRange(counter, len(self.instance_list)) self.pgbar.setValue(0) for instance in self.instance_list: import_status = False counter = counter + 1 self.parent_ids = {} entity_importer = EntityImporter(instance) group_identifier = entity_importer.instance_group_id() #set the geometry coordinate system entity_importer.geomsetter(self.on_projection_select()) self.archive_this_import_file(counter, instance) if has_relations: #Import parents table first for parent_table in self.relations.keys(): cu_obj = parent_table if parent_table in self.instance_entities(): ref_id, import_status = entity_importer.process_parent_entity_import( parent_table) if group_identifier: self.parent_ids[parent_table] = [ ref_id, group_identifier ] else: self.parent_ids[parent_table] = [ ref_id, parent_table ] log_timestamp = '{0} -- parent table import succeeded: {1}'\ .format(parent_table, str(import_status)) self.log_table_entry(log_timestamp) parents_info.append(parent_table) if parent_table[1] in entity_info: entity_info.remove(parent_table) for table in entity_info: cu_obj = table if table not in parents_info: table_id, status = entity_importer.process_import_to_db( table, self.parent_ids) if table in self.parent_ids: continue else: self.parent_ids[table] = [ table_id, group_identifier ] self.log_table_entry( " -- {0} import succeeded: ".format(cu_obj) + str(status)) self.txt_feedback.append( 'saving record "{0}" to database'.format(counter)) if self.uuid_extractor.has_str_captured_in_instance(): if self.parent_ids is not None: entity_importer.process_social_tenure( self.parent_ids) self.log_table_entry( " -- saving social tenure relationship") self.pgbar.setValue(counter) self.txt_feedback.append( 'Number of record successfully imported: {}'.format( counter)) else: self._notif_bar_str.insertErrorNotification( "No user selected entities to import") self.pgbar.setValue(0) return except Exception as ex: self.log_table_entry( unicode(ex.message) + '-- {0} import succeeded: '.format(cu_obj) + unicode(import_status)) self.feedback_message(unicode(ex.message)) return def has_foreign_keys_parent(self, select_entities): """ Ensure we check that the table is not parent else import parent table first :return: """ self.relations = {} str_tables = current_profile().social_tenure party_tbl = str_tables.parties[0].name sp_tbl = str_tables.spatial_units[0].name has_relations = False for table in select_entities: table_object = current_profile().entity_by_name(table) cols = table_object.columns.values() for col in cols: if col.TYPE_INFO == 'FOREIGN_KEY': parent_object = table_object.columns[col.name] if parent_object.parent: self.relations[parent_object.parent.name] = [ table, col.name ] has_relations = True else: self.feedback_message( 'unable to read foreign key properties for "{0}"'. format(parent_object.name)) return has_str_defined = self.uuid_extractor.has_str_captured_in_instance() if has_str_defined: if party_tbl not in self.relations.keys(): self.relations[party_tbl] = [ 'social_tenure_relationship', str_tables.parties[0].short_name.lower() + '_id' ] if sp_tbl not in self.relations.keys(): self.relations[sp_tbl] = [ 'social_tenure_relationship', str_tables.spatial_units[0].short_name.lower() + '_id' ] else: return has_relations def parent_table_isselected(self): """ Take note that the user selected tables may or may not be imported based on parent child table relationship :return: """ try: silent_list = [] if self.user_selected_entities() > 0: for table in self.relations.keys(): if table not in self.user_selected_entities(): silent_list.append(table[1]) return silent_list except Exception as ex: self._notif_bar_str.insertErrorNotification(ex.message) def archive_this_import_file(self, counter, instance): """ Ensure that only import are done once :return: """ try: self.importlogger.logger_sections() file_info = 'File instance ' + str(counter) + ' : \n' + instance self.importlogger.onlogger_action(file_info) except IOError as io: self._notif_bar_str.insertErrorNotification(MSG + ": " + io.message) pass def log_table_entry(self, instance): """ Ensure that only import are done once :return: """ try: current_time = QDateTime() import_time = current_time.currentDateTime() log_entry = instance + ' ' + str(import_time.toPyDateTime()) self.importlogger.onlogger_action(log_entry) except IOError as io: self._notif_bar_str.insertErrorNotification(MSG + ": " + io.message) pass def check_previous_import(self): """ Ensure we are importing files once :return: """ try: self.importlogger.add_log_info() for files in self.instance_list: current_dir = os.path.basename(files) exist = self.importlogger.check_file_exist(current_dir) if exist: self.instance_list.remove(files) self.txt_count.setText(str(len(self.instance_list))) if self.record_count() != len(self.instance_list): msg = 'Some files have been already imported and therefore ' \ 'not enumerated' self._notif_bar_str.insertErrorNotification(msg) except IOError as io: self._notif_bar_str.insertErrorNotification(MSG + ": " + io.message) pass def available_records(self): """ Let the user know how many records have been collected and are available for inport process :return: """ self.txt_count.setText(unicode(self.record_count())) def record_count(self): """ get the count of instance dir in the selected directory :return: integer """ return len([ name for name in os.listdir(self.path) if os.path.isdir(os.path.join(self.path, name)) if name.startswith(self.profile_formater()) ]) def profile_formater(self): """ Format the profile name by removing underscore character :return: """ if self.txt_filter.text() != '': filter_text = self.txt_filter.text() return filter_text else: return self.profile def update_files_with_custom_filter(self): """ Get the new file count with the user custom filter text :return: file count """ self.available_records() self.on_dir_path() self.profile_instance_entities() def projection_settings(self): """ let user select the projections for the data :return: """ project_select = ProjectionSelector(self) projection = project_select.loadAvailableSystems() self.txt_srid.setText(str(projection)) def on_projection_select(self): """ Get the selected projection and set it during data import :return: """ vals = self.txt_srid.text().split(":") return vals[1] def on_directory_search(self): """ Let the user choose the directory with instances :return: """ home_path = 'home' if self.txt_directory.text() != '': home_path = self.txt_directory.text() dir_name = QFileDialog.getExistingDirectory(self, 'Open Directory', home_path, QFileDialog.ShowDirsOnly) if dir_name: self.txt_directory.setText(str(dir_name)) self.current_profile_changed() self.check_state_on() def feedback_message(self, msg): """ Create a dialog box to capture and display errrors related to db while importing data :param: msg :type: string :return:Qdialog """ msgbox = QMessageBox() msgbox.setStandardButtons(QMessageBox.Ok | QMessageBox.No) msgbox.setWindowTitle("Data Import") msgbox.setText(msg) msgbox.exec_() msgbox.show() return msgbox def accept(self): """ Execute the import dialog once the save button has been clicked :return: """ # if self.tab_widget.currentIndex() == 1: # host = self.txt_host.text() # passwd = self.txt_pass.text() # db = self.cbo_dbname.currentText() # user = self.txt_username.text() # port = self.txt_port.text() # json_extractor = JSONEXTRACTOR(user,passwd,host,port,db, self.profile) # json_conn = json_extractor.create_orphan_connection() # self.importlogger.onlogger_action(json_conn) # self.txt_svlog.append(json_conn) #else: self.buttonBox.setEnabled(False) try: if self.lst_widget.count() < 1: msg = 'No mobile records could be found for the current profile' self._notif_bar_str.insertErrorNotification(msg) self.buttonBox.setEnabled(True) return entities = self.user_selected_entities() if len(entities) < 1: if QMessageBox.information( self, QApplication.translate('MobileForms', 'Import Warning'), QApplication.translate( 'MobileForms', 'You have not ' 'selected any entity for import. All entities ' 'will be imported'), QMessageBox.Ok | QMessageBox.No) == QMessageBox.Ok: entities = self.instance_entities() else: return self.entity_attribute_to_database(entities) self.buttonBox.setEnabled(True) except Exception as ex: self._notif_bar_str.insertErrorNotification(ex.message) self.feedback_message(str(ex.message)) self.buttonBox.setEnabled(True)
class TemplateDocumentSelector(QDialog,Ui_frmDocumentSelector): """ Dialog for selecting a document template from the saved list. """ def __init__(self, parent=None,selectMode=True, filter_data_source=''): QDialog.__init__(self,parent) self.setupUi(self) self.notifBar = NotificationBar(self.vlNotification) self._mode = selectMode #Filter templates by the specified table name self._filter_data_source = filter_data_source #Document templates in current profile self._profile_templates = [] self._current_profile = current_profile() #Load current profile templates self._load_current_profile_templates() if selectMode: self.buttonBox.setVisible(True) self.manageButtonBox.setVisible(False) currHeight = self.size().height() self.resize(200,currHeight) else: self.buttonBox.setVisible(False) self.manageButtonBox.setVisible(True) self.setWindowTitle( QApplication.translate( "TemplateDocumentSelector", "Template Manager" ) ) #Configure manage buttons btnEdit = self.manageButtonBox.button(QDialogButtonBox.Ok) btnEdit.setText(QApplication.translate("TemplateDocumentSelector","Edit...")) btnEdit.setIcon(QIcon(":/plugins/stdm/images/icons/edit.png")) btnDelete = self.manageButtonBox.button(QDialogButtonBox.Save) btnDelete.setText(QApplication.translate("TemplateDocumentSelector","Delete")) btnDelete.setIcon(QIcon(":/plugins/stdm/images/icons/delete.png")) #Connect signals self.buttonBox.accepted.connect(self.onAccept) btnEdit.clicked.connect(self.onEditTemplate) btnDelete.clicked.connect(self.onDeleteTemplate) #Get saved document templates then add to the model templates = documentTemplates() self._docItemModel = QStandardItemModel(parent) self._docItemModel.setColumnCount(2) #Append current profile templates to the model. for dt in self._profile_templates: if self._template_contains_filter_table(dt): doc_name_item = self._createDocNameItem(dt.name) file_path_item = QStandardItem(dt.path) self._docItemModel.appendRow([doc_name_item,file_path_item]) self.lstDocs.setModel(self._docItemModel) def _load_current_profile_templates(self): # Loads only those templates that refer to tables in the current # profile. if self._current_profile is None: return #Get saved document templates then add to the model templates = documentTemplates() profile_tables = self._current_profile.table_names() #Get templates for the current profile for name, path in templates.iteritems(): doc_temp = _DocumentTemplate.build_from_path(name, path) if doc_temp.data_source is None: continue #Assert data source is in the current profile if doc_temp.data_source.referenced_table_name in profile_tables: self._add_doc_temp(doc_temp) #self._profile_templates.append(doc_temp) if doc_temp.data_source._dataSourceName in user_non_profile_views(): self._add_doc_temp(doc_temp) #self._profile_templates.append(doc_temp) def _add_doc_temp(self, doc_temp): found = False for template in self._profile_templates: if template.name == doc_temp.name: found = True break if not found: self._profile_templates.append(doc_temp) def _template_contains_filter_table(self, document_template): #Returns true if the template refers to the filter data source #If no filter data source defined then always return True if document_template.data_source._dataSourceName in user_non_profile_views(): return True if not self._filter_data_source: return True referenced_table = document_template.referenced_table_name if referenced_table == self._filter_data_source: return True return False @property def mode(self): return self._mode @property def filter_data_source(self): return self._filter_data_source def _createDocNameItem(self,docName): """ Create a template document standard item. """ #Set icon icon = QIcon() icon.addPixmap( QPixmap( ":/plugins/stdm/images/icons/document.png" ), QIcon.Normal, QIcon.Off ) dnItem = QStandardItem(icon,docName) return dnItem def onEditTemplate(self): """ Slot raised to edit document template. """ self.notifBar.clear() if self.documentMapping() == None: self.notifBar.insertErrorNotification(QApplication.translate("TemplateDocumentSelector", \ "Please select a document template to edit")) return templateName,filePath = self.documentMapping() docName,ok = QInputDialog.getText(self, \ QApplication.translate("TemplateDocumentSelector","Edit Template"), \ QApplication.translate("TemplateDocumentSelector","Please enter the new template name below"), \ text = templateName) if ok and docName == "": self.notifBar.insertErrorNotification(QApplication.translate("TemplateDocumentSelector", \ "Template name cannot be empty")) return elif docName == templateName: return elif ok and docName != "": result,newTemplatePath = self._editTemplate(filePath, docName) if result: #Update view mIndices = self._selectedMappings() docNameItem = self._docItemModel.itemFromIndex(mIndices[0]) filePathItem = self._docItemModel.itemFromIndex(mIndices[1]) docNameItem.setText(docName) filePathItem.setText(newTemplatePath) self.notifBar.insertSuccessNotification(QApplication.translate("TemplateDocumentSelector", \ "'{0}' template has been successfully updated".format(docName))) else: self.notifBar.insertErrorNotification(QApplication.translate("TemplateDocumentSelector", \ "Error: '{0}' template could not be updated".format(templateName))) def onDeleteTemplate(self): """ Slot raised to delete document template. """ self.notifBar.clear() if self.documentMapping() == None: self.notifBar.insertErrorNotification(QApplication.translate("TemplateDocumentSelector", \ "Please select a document template to delete")) return templateName,filePath = self.documentMapping() result = QMessageBox.warning(self, QApplication.translate("TemplateDocumentSelector", \ "Confirm delete"), QApplication.translate("TemplateDocumentSelector", \ "Are you sure you want to delete '{0}' template?" \ "This action cannot be undone.\nClick Yes to proceed " \ "or No to cancel.".format(templateName)), QMessageBox.Yes|QMessageBox.No) if result == QMessageBox.No: return status = self._deleteDocument(filePath) if status: #Remove item from list using model index row number selectedDocNameIndices = self.lstDocs.selectionModel().selectedRows(0) row = selectedDocNameIndices[0].row() self._docItemModel.removeRow(row) self.notifBar.insertSuccessNotification(QApplication.translate("TemplateDocumentSelector", \ "'{0}' template has been successfully removed".format(templateName))) else: self.notifBar.insertErrorNotification(QApplication.translate("TemplateDocumentSelector", \ "Error: '{0}' template could not be removed".format(templateName))) def onAccept(self): """ Slot raised to close the dialog only when a selection has been made by the user. """ self.notifBar.clear() if self.documentMapping() == None: self.notifBar.insertErrorNotification(QApplication.translate("TemplateDocumentSelector", \ "Please select a document")) return self.accept() def _selectedMappings(self): """ Returns the model indices for the selected row. """ selectedDocNameIndices = self.lstDocs.selectionModel().selectedRows(0) selectedFilePathIndices = self.lstDocs.selectionModel().selectedRows(1) if len(selectedDocNameIndices) == 0: return None docNameIndex = selectedDocNameIndices[0] filePathIndex = selectedFilePathIndices[0] return (docNameIndex,filePathIndex) def documentMapping(self): """ Returns a tuple containing the selected document name and the corresponding file name. """ mIndices = self._selectedMappings() if mIndices == None: return None docNameItem = self._docItemModel.itemFromIndex(mIndices[0]) filePathItem = self._docItemModel.itemFromIndex(mIndices[1]) return (docNameItem.text(),filePathItem.text()) def _editTemplate(self,templatePath,newName): """ Updates the template document to use the new name. """ templateFile = QFile(templatePath) if not templateFile.open(QIODevice.ReadOnly): QMessageBox.critical(self, QApplication.translate("TemplateDocumentSelector","Open Operation Error"), \ "{0}\n{1}".format(QApplication.translate("TemplateDocumentSelector","Cannot read template file."), \ templateFile.errorString() )) return (False,"") templateDoc = QDomDocument() if templateDoc.setContent(templateFile): composerElement = templateDoc.documentElement() titleAttr = composerElement.attributeNode("_title") if not titleAttr.isNull(): titleAttr.setValue(newName) #Try remove file status = templateFile.remove() if not status: return (False,"") #Create new file newTemplatePath = self._composerTemplatesPath() + "/" + newName + ".sdt" newTemplateFile = QFile(newTemplatePath) if not newTemplateFile.open(QIODevice.WriteOnly): QMessageBox.critical(self, QApplication.translate("TemplateDocumentSelector","Save Operation Error"), \ "{0}\n{1}".format(QApplication.translate("TemplateDocumentSelector","Could not save template file."), \ newTemplateFile.errorString() )) return (False,"") if newTemplateFile.write(templateDoc.toByteArray()) == -1: QMessageBox.critical(self, QApplication.translate("TemplateDocumentSelector","Save Error"), \ QApplication.translate("TemplateDocumentSelector","Could not save template file.")) return (False,"") newTemplateFile.close() return (True,newTemplatePath) def _deleteDocument(self,templatePath): """ Delete the document template from the file system. """ docFile = QFile(templatePath) return docFile.remove() def _composerTemplatesPath(self): """ Reads the path of composer templates in the registry. """ regConfig = RegistryConfig() keyName = "ComposerTemplates" valueCollection = regConfig.read([keyName]) if len(valueCollection) == 0: return None else: return valueCollection[keyName]
class GeoODKConverter(QDialog, FORM_CLASS): def __init__(self, parent=None): """Class Constructor.""" super(GeoODKConverter, self).__init__(parent) # Set up the user interface from Designer. # After setupUI you can access any designer object by doing # self.<objectname>, and you can use autoconnect slots - see # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html # #widgets-and-dialogs-with-aut o-connect self.connect_action = pyqtSignal(str) self.setupUi(self) self.chk_all.setCheckState(Qt.Checked) self.entity_model = EntitiesModel() self.set_entity_model_view(self.entity_model) self.stdm_config = None self.parent = parent self.load_profiles() self.check_state_on() self.check_geoODK_path_exist() self.chk_all.stateChanged.connect(self.check_state_on) self.btnShowOutputFolder.clicked.connect(self.onShowOutputFolder) #self.btn_upload.clicked.connect(self.upload_generated_form) self._notif_bar_str = NotificationBar(self.vlnotification) def onShowOutputFolder(self): output_path = FORM_HOME # windows if sys.platform.startswith('win32'): os.startfile(output_path) # *nix systems if sys.platform.startswith('linux'): subprocess.Popen(['xdg-open', output_path]) # macOS if sys.platform.startswith('darwin'): subprocess.Popen(['open', output_path]) def check_state_on(self): """ Ensure all the items in the list are checked :return: """ if self.entity_model.rowCount() > 0: for row in range(self.entity_model.rowCount()): item = self.entity_model.item(row) if self.chk_all.isChecked(): item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) def load_profiles(self): """ Read and load profiles from StdmConfiguration instance """ self.populate_view_models(current_profile()) def profiles(self): """ Get all profiles :return: """ return self.load_config().values() def populate_view_models(self, profile): for entity in profile.entities.values(): if entity.action == DbItem.DROP: continue if hasattr(entity, 'user_editable') and entity.TYPE_INFO <> 'VALUE_LIST': if entity.user_editable == False: continue if entity.TYPE_INFO not in ['SUPPORTING_DOCUMENT', 'SOCIAL_TENURE', 'ADMINISTRATIVE_SPATIAL_UNIT', 'ENTITY_SUPPORTING_DOCUMENT', 'ASSOCIATION_ENTITY', 'AUTO_GENERATE_CODE']: if entity.TYPE_INFO == 'VALUE_LIST': pass else: self.entity_model.add_entity(entity) self.set_model_items_selectable() def set_entity_model_view(self, entity_model): """ Set our list view to the default model :return: """ self.trentities.setModel(entity_model) def set_model_items_selectable(self): """ Ensure that the entities are checkable :return: """ if self.entity_model.rowCount() >0: for row in range(self.entity_model.rowCount()): index = self.entity_model.index(row,0) item_index = self.entity_model.itemFromIndex(index) item_index.setCheckable(True) def selected_entities_from_Model(self): """ Get selected entities for conversion to Xform from the user selection :return: """ entity_list =[] if self.entity_model.rowCount() > 0: for row in range(self.entity_model.rowCount()): item = self.entity_model.item(row) if item.isCheckable() and item.checkState() == Qt.Checked: entity_list.append(item.text()) return entity_list def check_geoODK_path_exist(self): """ Check if the geoodk paths are there in the directory Otherwise create them :return: """ if not os.access(FORM_HOME, os.F_OK): os.makedirs(unicode(FORM_HOME)) def upload_generated_form(self): """ Upload the generated Xform file to mobile phone. This eliminates the process of copying the file manually to the mobile device :return: """ form_uploader = FormUploader(self) form_uploader.exec_() def generate_mobile_form(self, selected_entities): """ Generate mobile form based on the selected entities. :return: """ #try: self._notif_bar_str.clear() if len(selected_entities) == 0: self._notif_bar_str.insertErrorNotification( 'No entity selected. Please select at least one entity...' ) return if len(selected_entities) > 0: geoodk_writer = GeoodkWriter(selected_entities, self.str_supported) geoodk_writer.write_data_to_xform() msg = 'File saved in: {}' self._notif_bar_str.insertInformationNotification(msg.format(FORM_HOME)) #except Exception as ex: # self._notif_bar_str.insertErrorNotification(ex.message + # ': Unable to generate Mobile Form') # return def accept(self): """ Generate mobile forms based on user selected entities. Check if str is enabled, then ensure str tables are enabled. :return: """ user_entities = self.selected_entities_from_Model() self.str_supported = False if self.ck_social_tenure.isChecked(): self.str_supported = True str_definition = current_profile().social_tenure str_definition_party = str_definition.parties[0].short_name str_definition_spatial = str_definition.spatial_units[0].short_name if str_definition_party not in user_entities or str_definition_spatial not in user_entities: self._notif_bar_str.insertErrorNotification( 'One of the entities required to define str is not selected. Form not saved' ) return #else: #self.generate_mobile_form(user_entities) #else: self.generate_mobile_form(user_entities)
class ProfileEditor(QDialog, Ui_Profile): def __init__(self, parent): QDialog.__init__(self, parent) self.profile_name = '' self.desc = '' self.setupUi(self) self.init_controls() self.notice_bar = NotificationBar(self.notif_bar) def init_controls(self): self.edtProfile.clear() self.edtDesc.clear() self.edtProfile.setFocus() self.edtProfile.textChanged.connect(self.validate_text) def format_name(self, txt): ''''remove any trailing spaces in the name and replace them underscore''' formatted_name = txt.strip().replace(' ', "_") return formatted_name def add_profile(self): self.profile_name = self.format_name(unicode(self.edtProfile.text())) self.desc = unicode(self.edtDesc.text()) def show_notification(self, message): """ Shows a warning notification bar message. :param message: The message of the notification. :type message: String """ msg = self.trUtf8(message) self.notice_bar.clear() self.notice_bar.insertErrorNotification(msg) def validate_text(self, text): """ Validates and updates the entered text if necessary. Spaces are replaced by _ and capital letters are replaced by small. :param text: The text entered :type text: String """ text_edit = self.sender() cursor_position = text_edit.cursorPosition() text_edit.setValidator(None) if len(text) == 0: return locale = QSettings().value("locale/userLocale")[0:2] if locale == 'en': name_regex = QRegExp('^(?=.{0,40}$)[ _a-zA-Z][a-zA-Z0-9_ ]*$') name_validator = QRegExpValidator(name_regex) text_edit.setValidator(name_validator) QApplication.processEvents() last_character = text[-1:] state = name_validator.validate(text, text.index(last_character))[0] if state != QValidator.Acceptable: msg = u'\'{0}\' is not allowed at this position.'.format( last_character) self.show_notification(msg) text = text[:-1] # remove space and underscore at the beginning of the text if len(text) > 1: if text[0] == ' ' or text[0] == '_': text = text[1:] self.blockSignals(True) text_edit.setText(text) text_edit.setCursorPosition(cursor_position) self.blockSignals(False) text_edit.setValidator(None) def accept(self): '''listen to user action on the dialog''' if self.edtProfile.text() == '' or self.edtProfile.text() == ' ': self.error_info_message( QApplication.translate("ProfileEditor", "Please enter a valid Profile name.")) return self.add_profile() self.done(1) def reject(self): self.done(0) def error_info_message(self, Message): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("STDM") msg.setText(Message) msg.exec_()
class DocumentGeneratorDialog(WIDGET, BASE): """ Dialog that enables a user to generate documents by using configuration information for different entities. """ def __init__(self, iface, access_templates, parent=None, plugin=None): QDialog.__init__(self, parent) self.setupUi(self) self.btnSelectTemplate.setIcon(GuiUtils.get_icon('document.png')) self._iface = iface self.plugin = plugin self._docTemplatePath = "" self._outputFilePath = "" self.curr_profile = current_profile() self.last_data_source = None self._config_mapping = OrderedDict() self._notif_bar = NotificationBar(self.vlNotification) self._doc_generator = DocumentGenerator(self._iface, self) self._data_source = "" self.access_templates = access_templates enable_drag_sort(self.lstDocNaming) # Configure generate button generateBtn = self.buttonBox.button(QDialogButtonBox.Ok) if not generateBtn is None: generateBtn.setText( QApplication.translate("DocumentGeneratorDialog", "Generate")) # Load supported image types supportedImageTypes = QImageWriter.supportedImageFormats() for imageType in supportedImageTypes: imageTypeStr = imageType.data().decode() self.cboImageType.addItem(imageTypeStr) self._init_progress_dialog() # Connect signals self.btnSelectTemplate.clicked.connect(self.onSelectTemplate) self.buttonBox.accepted.connect(self.onGenerate) self.chkUseOutputFolder.stateChanged.connect( self.onToggledOutputFolder) self.rbExpImage.toggled.connect(self.onToggleExportImage) self.tabWidget.currentChanged.connect(self.on_tab_index_changed) self.chk_template_datasource.stateChanged.connect( self.on_use_template_datasource) self.btnShowOutputFolder.clicked.connect(self.onShowOutputFolder) def _init_progress_dialog(self): """ Initializes the progress dialog. """ self.progress = QProgressBar(self) self.progress.resize(self.width(), 10) self.progress.setTextVisible(False) def add_entity_configuration(self, **kwargs): ent_config = EntityConfig(**kwargs) self.add_entity_config(ent_config) def add_entity_config(self, ent_config, progress_value=0): QApplication.processEvents() if not self._config_mapping.get(ent_config.title(), ""): fk_mapper = self._create_fk_mapper(ent_config) self.tabWidget.addTab(fk_mapper, ent_config.title()) self._config_mapping[ent_config.title()] = ent_config # Force list of column names to be loaded if self.tabWidget.currentIndex() != 0: self.tabWidget.setCurrentIndex(0) else: self.on_tab_index_changed(0) self.progress.setValue(progress_value) def on_tab_index_changed(self, index): if index == -1: return config = self.config(index) if not config is None: # Set data source name self._data_source = config.data_source() def on_use_template_datasource(self, state): if state == Qt.Checked: self.tabWidget.setEnabled(False) self.chkUseOutputFolder.setEnabled(False) self.chkUseOutputFolder.setChecked(False) elif state == Qt.Unchecked: self.tabWidget.setEnabled(True) self.chkUseOutputFolder.setEnabled(True) self.chkUseOutputFolder.setChecked(False) self.on_tab_index_changed(self.tabWidget.currentIndex()) def onShowOutputFolder(self): reg_config = RegistryConfig() path = reg_config.read([COMPOSER_OUTPUT]) output_path = path.get(COMPOSER_OUTPUT, '') # windows if sys.platform.startswith('win32'): os.startfile(output_path) # *nix systems if sys.platform.startswith('linux'): subprocess.Popen(['xdg-open', output_path]) # macOS if sys.platform.startswith('darwin'): subprocess.Popen(['open', output_path]) def notification_bar(self): """ :return: Returns an instance of the notification bar. :rtype: NotificationBar """ return self._notif_bar def config(self, index): """ Returns the configuration for the current mapper in the tab widget. """ tab_key = self.tabWidget.tabText(index) return self._config_mapping.get(tab_key, None) def current_config(self): """ Returns the configuration corresponding to the current widget in the tab. """ return self.config(self.tabWidget.currentIndex()) def _load_model_columns(self, config): """ Load model columns into the view for specifying file output name. Only those columns of display type variants will be used. """ model_attr_mapping = OrderedDict() for c in config.data_source_columns(): model_attr_mapping[c] = format_name(c) self.lstDocNaming.load_mapping(model_attr_mapping) def _load_data_source_columns(self, entity): """ Load the columns of a data source for use in the file naming. """ table_cols = entity_display_columns(entity, True) attr_mapping = OrderedDict() for c, header in table_cols.items(): attr_mapping[c] = header self.lstDocNaming.load_mapping(attr_mapping) def _create_fk_mapper(self, config): fk_mapper = ForeignKeyMapper(config.ds_entity, self.tabWidget, self._notif_bar, enable_list=True, can_filter=True, plugin=self.plugin) fk_mapper.setDatabaseModel(config.model()) fk_mapper.setSupportsList(True) fk_mapper.setDeleteonRemove(False) fk_mapper.setNotificationBar(self._notif_bar) return fk_mapper def onSelectTemplate(self): """ Slot raised to load the template selector dialog. """ current_config = self.current_config() if current_config is None: msg = QApplication.translate( 'DocumentGeneratorDialog', 'An error occured while trying to determine the data source ' 'for the current entity.\nPlease check your current profile ' 'settings.') QMessageBox.critical( self, QApplication.translate('DocumentGeneratorDialog', 'Template Selector'), msg) return # Set the template selector to only load those templates that # reference the current data source. filter_table = current_config.data_source() templateSelector = TemplateDocumentSelector( self, filter_data_source=filter_table, access_templates=self.access_templates) if templateSelector.exec_() == QDialog.Accepted: docName, docPath = templateSelector.documentMapping() self.lblTemplateName.setText(docName) self._docTemplatePath = docPath if filter_table != self.last_data_source: # Load template data source fields self._load_template_datasource_fields() def _load_template_datasource_fields(self): # If using template data source template_doc, err_msg = self._doc_generator.template_document( self._docTemplatePath) if template_doc is None: QMessageBox.critical( self, "Error Generating documents", QApplication.translate( "DocumentGeneratorDialog", "Error Generating documents - %s" % (err_msg))) return composer_ds, err_msg = self._doc_generator.composer_data_source( template_doc) if composer_ds is None: QMessageBox.critical( self, "Error Generating documents", QApplication.translate( "DocumentGeneratorDialog", "Error Generating documents - %s" % (err_msg))) return # Load data source columns self._data_source = self.current_config().data_source() self.ds_entity = self.curr_profile.entity_by_name(self._data_source) self._load_data_source_columns(self.ds_entity) def onToggledOutputFolder(self, state): """ Slot raised to enable/disable the generated output documents to be written to the plugin composer output folder using the specified naming convention. """ if state == Qt.Checked: self.gbDocNaming.setEnabled(True) elif state == Qt.Unchecked: self.gbDocNaming.setEnabled(False) def reset(self, success_status=False): """ Clears/resets the dialog from user-defined options. """ self._docTemplatePath = "" self._data_source = "" self.lblTemplateName.setText("") # reset form only if generation is successful if success_status: fk_table_view = self.tabWidget.currentWidget(). \ findChild(QTableView) while fk_table_view.model().rowCount() > 0: fk_table_view.model().rowCount(0) fk_table_view.model().removeRow(0) if self.tabWidget.count() > 0 and \ not self.chk_template_datasource.isChecked(): self.on_tab_index_changed(0) if self.cboImageType.count() > 0: self.cboImageType.setCurrentIndex(0) def onToggleExportImage(self, state): """ Slot raised to enable/disable the image formats combobox. """ if state: self.cboImageType.setEnabled(True) else: self.cboImageType.setEnabled(False) def onGenerate(self): """ Slot raised to initiate the certificate generation process. """ self._notif_bar.clear() success_status = True config = self.current_config() self.last_data_source = config.data_source() if config is None: self._notif_bar.insertErrorNotification(QApplication.translate("DocumentGeneratorDialog", \ "The entity configuration could not be extracted.")) return # Get selected records and validate records = self.tabWidget.currentWidget().entities() if self.chk_template_datasource.isChecked(): records = self._dummy_template_records() if len(records) == 0: self._notif_bar.insertErrorNotification(QApplication.translate("DocumentGeneratorDialog", \ "Please load at least one entity record")) return if not self._docTemplatePath: self._notif_bar.insertErrorNotification(QApplication.translate("DocumentGeneratorDialog", \ "Please select a document template to use")) return documentNamingAttrs = self.lstDocNaming.selectedMappings() if self.chkUseOutputFolder.checkState() == Qt.Checked and len( documentNamingAttrs) == 0: self._notif_bar.insertErrorNotification(QApplication.translate("DocumentGeneratorDialog", \ "Please select at least one field for naming the output document")) return # Set output file properties if self.rbExpImage.isChecked(): outputMode = DocumentGenerator.Image fileExtension = self.cboImageType.currentText() saveAsText = "Image File" else: outputMode = DocumentGenerator.PDF fileExtension = "pdf" saveAsText = "PDF File" # Show save file dialog if not using output folder if self.chkUseOutputFolder.checkState() == Qt.Unchecked: docDir = source_document_location() if self._outputFilePath: fileInfo = QFileInfo(self._outputFilePath) docDir = fileInfo.dir().path() self._outputFilePath, _ = QFileDialog.getSaveFileName( self, QApplication.translate("DocumentGeneratorDialog", "Save Document"), docDir, "{0} (*.{1})".format( QApplication.translate("DocumentGeneratorDialog", saveAsText), fileExtension)) if not self._outputFilePath: self._notif_bar.insertErrorNotification( QApplication.translate( "DocumentGeneratorDialog", "Process aborted. No output file was specified.")) return # Include extension in file name self._outputFilePath = self._outputFilePath # + "." + fileExtension # else: # Multiple files to be generated. # pass self._doc_generator.set_link_field(config.link_field()) self._doc_generator.clear_attr_value_formatters() if not self.chk_template_datasource.isChecked(): # Apply cell formatters for naming output files self._doc_generator.set_attr_value_formatters(config.formatters()) entity_field_name = "id" # Iterate through the selected records progressDlg = QProgressDialog(self) progressDlg.setMaximum(len(records)) try: QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) for i, record in enumerate(records): progressDlg.setValue(i) if progressDlg.wasCanceled(): success_status = False break # User-defined location if self.chkUseOutputFolder.checkState() == Qt.Unchecked: status, msg = self._doc_generator.run( self._docTemplatePath, entity_field_name, record.id, outputMode, data_source=self.ds_entity.name, filePath=self._outputFilePath) self._doc_generator.clear_temporary_layers() # Output folder location using custom naming else: status, msg = self._doc_generator.run( self._docTemplatePath, entity_field_name, record.id, outputMode, dataFields=documentNamingAttrs, fileExtension=fileExtension, data_source=self.ds_entity.name) self._doc_generator.clear_temporary_layers() if not status: result = QMessageBox.warning( self, QApplication.translate("DocumentGeneratorDialog", "Document Generate Error"), msg, QMessageBox.Ignore | QMessageBox.Abort) if result == QMessageBox.Abort: progressDlg.close() success_status = False # Restore cursor QApplication.restoreOverrideCursor() return # If its the last record and user has selected to ignore if i + 1 == len(records): progressDlg.close() success_status = False # Restore cursor QApplication.restoreOverrideCursor() return else: progressDlg.setValue(len(records)) QApplication.restoreOverrideCursor() QMessageBox.information( self, QApplication.translate("DocumentGeneratorDialog", "Document Generation Complete"), QApplication.translate( "DocumentGeneratorDialog", "Document generation has successfully completed.")) except DummyException as ex: LOGGER.debug(str(ex)) err_msg = sys.exc_info()[1] QApplication.restoreOverrideCursor() QMessageBox.critical( self, "STDM", QApplication.translate( "DocumentGeneratorDialog", "Error Generating documents - %s" % (err_msg))) success_status = False # Reset UI self.reset(success_status) def _dummy_template_records(self): """ This is applied when records from a template data source are to be used to generate the documents where no related entity will be used to filter matching records in the data source. The iteration of the data source records will be done internally within the DocumentGenerator class. """ class _DummyRecord: id = 1 return [_DummyRecord()] def showEvent(self, event): """ Notifies if there are not entity configuration objects defined. :param event: Window event :type event: QShowEvent """ QTimer.singleShot(500, self.check_entity_config) return QDialog.showEvent(self, event) def check_entity_config(self): if len(self._config_mapping) == 0: self._notif_bar.clear() msg = QApplication.translate( "DocumentGeneratorDialog", "Table " "configurations do not exist or have not been configured properly" ) self._notif_bar.insertErrorNotification(msg)
class ColumnEditor(QDialog, Ui_ColumnEditor): """ Dialog to add/edit entity columns """ def __init__(self, **kwargs): """ :param parent: Owner of this dialog :type parent: QWidget :param kwargs: Keyword dictionary of the following parameters; column - Column you editing, None if its a new column entity - Entity you are adding the column to profile - Current profile in_db - Boolean flag to indicate if a column has been created in the database auto_add- True to automatically add a new column to the entity, default is False. """ self.form_parent = kwargs.get('parent', self) self.column = kwargs.get('column', None) self.entity = kwargs.get('entity', None) self.profile = kwargs.get('profile', None) self.in_db = kwargs.get('in_db', False) self.is_new = kwargs.get('is_new', True) self.auto_entity_add = kwargs.get('auto_add', False) QDialog.__init__(self, self.form_parent) self.FK_EXCLUDE = [u'supporting_document', u'admin_spatial_unit_set'] self.EX_TYPE_INFO = ['SUPPORTING_DOCUMENT', 'SOCIAL_TENURE', 'ADMINISTRATIVE_SPATIAL_UNIT', 'ENTITY_SUPPORTING_DOCUMENT', 'VALUE_LIST', 'ASSOCIATION_ENTITY', 'AUTO_GENERATED'] self.setupUi(self) self.dtypes = {} self.type_info = '' # dictionary to hold default attributes for each data type self.type_attribs = {} self.init_type_attribs() # dictionary to act as a work area for the form fields. self.form_fields = {} self.init_form_fields() self.fk_entities = [] self.lookup_entities = [] # Exclude column type info in the list self._exclude_col_type_info = [] if self.is_new: self.prop_set = None # why not False?? else: self.prop_set = True # the current entity should not be part of the foreign key parent table, # add it to the exclusion list self.FK_EXCLUDE.append(self.entity.short_name) self.type_names = \ [unicode(name) for name in BaseColumn.types_by_display_name().keys()] self.cboDataType.currentIndexChanged.connect(self.change_data_type) self.btnColProp.clicked.connect(self.data_type_property) self.edtColName.textChanged.connect(self.validate_text) self.notice_bar = NotificationBar(self.notif_bar) self.init_controls() def exclude_column_types(self, type_info): """ Exclude the column types with the given type_info. :param type_info: List of TYPE_INFO of columns to exclude. :type type_info: list """ self._exclude_col_type_info = type_info # Block index change signal of combobox self.cboDataType.blockSignals(True) # Reload column data types self.populate_data_type_cbo() # Select column type if it had been specified if not self.column is None: text = self.column.display_name() self.cboDataType.setCurrentIndex(self.cboDataType.findText(text)) # Re-enable signals self.cboDataType.blockSignals(False) def show_notification(self, message): """ Shows a warning notification bar message. :param message: The message of the notification. :type message: String """ self.notice_bar.clear() self.notice_bar.insertErrorNotification(message) def _column_type_info(self, column): """ Check if column has TYPE_INFO attribute :param column: Entity column object :return: Column type. Otherwise None :rtype: String or None """ try: return column.TYPE_INFO except AttributeError: return None def init_controls(self): """ Initialize GUI controls default state when the dialog window is opened. """ self.populate_data_type_cbo() if not self.column is None: self.column_to_form(self.column) self.column_to_wa(self.column) self.edtColName.setFocus() self.edtColName.setEnabled(not self.in_db) self.cboDataType.setEnabled(not self.in_db) self.buttonBox.button(QtGui.QDialogButtonBox.Ok).clicked.connect(self.accept) self.buttonBox.button(QtGui.QDialogButtonBox.Cancel).clicked.connect(self.cancel) col_type = self._column_type_info(self.column) if not self.in_db and col_type == 'GEOMETRY': opts = self.type_attribs[col_type] self.cbMandt.setEnabled(opts['mandt']['enabled_state']) self.cbUnique.setEnabled(opts['unique']['enabled_state']) self.cbIndex.setEnabled(opts['index']['enabled_state']) else: self.cbMandt.setEnabled(not self.in_db) self.cbUnique.setEnabled(not self.in_db) self.cbIndex.setEnabled(not self.in_db) def validate_text(self, text): """ Validates and updates the entered text if necessary. Spaces are replaced by _ and capital letters are replaced by small. :param text: The text entered :type text: String """ text_edit = self.sender() cursor_position = text_edit.cursorPosition() text_edit.setValidator(None) if len(text) == 0: return name_regex = QtCore.QRegExp('^(?=.{0,40}$)[ _a-zA-Z][a-zA-Z0-9_ ]*$') name_validator = QtGui.QRegExpValidator(name_regex) text_edit.setValidator(name_validator) QApplication.processEvents() last_character = text[-1:] locale = QSettings().value("locale/userLocale")[0:2] #if locale == 'en': state = name_validator.validate(text, text.index(last_character))[0] if state != QValidator.Acceptable: self.show_notification(u'"{}" is not allowed at this position.'. format(last_character) ) text = text[:-1] # fix caps, _, and spaces if last_character.isupper(): text = text.lower() if last_character == ' ': text = text.replace(' ', '_') if len(text) > 1: if text[0] == ' ' or text[0] == '_': text = text[1:] text = text.replace(' ', '_').lower() self.blockSignals(True) text_edit.setText(text) text_edit.setCursorPosition(cursor_position) self.blockSignals(False) text_edit.setValidator(None) def column_to_form(self, column): """ Initializes form controls with Column data. :param column: BaseColumn instance :type column: BaseColumn """ text = column.display_name() self.cboDataType.setCurrentIndex(self.cboDataType.findText(text)) self.edtColName.setText(column.name) self.edtColDesc.setText(column.description) self.txt_form_label.setText(column.label) self.edtUserTip.setText(column.user_tip) self.cbMandt.setChecked(column.mandatory) self.cbSearch.setCheckState(self.bool_to_check(column.searchable)) self.cbUnique.setCheckState(self.bool_to_check(column.unique)) self.cbIndex.setCheckState(self.bool_to_check(column.index)) ti = self.current_type_info() ps = self.type_attribs[ti].get('prop_set', None) if ps is not None: self.type_attribs[ti]['prop_set']= self.prop_set def column_to_wa(self, column): """ Initialize 'work area' form_fields with column data. :param column: BaseColumn instance :type column: BaseColumn """ if column is not None: self.form_fields['colname'] = column.name self.form_fields['value'] = None self.form_fields['mandt'] = column.mandatory self.form_fields['search'] = column.searchable self.form_fields['unique'] = column.unique self.form_fields['index'] = column.index if hasattr(column, 'minimum'): self.form_fields['minimum'] = column.minimum self.form_fields['maximum'] = column.maximum if hasattr(column, 'srid'): self.form_fields['srid'] = column.srid self.form_fields['geom_type'] = column.geom_type if hasattr(column, 'entity_relation'): self.form_fields['entity_relation'] = column.entity_relation if hasattr(column, 'association'): self.form_fields['first_parent'] = column.association.first_parent self.form_fields['second_parent'] = column.association.second_parent if hasattr(column, 'min_use_current_date'): self.form_fields['min_use_current_date'] = column.min_use_current_date self.form_fields['max_use_current_date'] = column.max_use_current_date if hasattr(column, 'min_use_current_datetime'): self.form_fields['min_use_current_datetime'] = \ column.min_use_current_datetime self.form_fields['max_use_current_datetime'] = \ column.max_use_current_datetime if hasattr(column, 'prefix_source'): self.form_fields['prefix_source'] = column.prefix_source self.form_fields['columns'] = column.columns self.form_fields['column_separators'] = column.column_separators self.form_fields['leading_zero'] = column.leading_zero self.form_fields['separator'] = column.separator self.form_fields['colname'] = column.name self.form_fields['enable_editing'] = column.enable_editing self.form_fields['disable_auto_increment'] = column.disable_auto_increment self.form_fields['hide_prefix'] = column.hide_prefix # Decimal properties if hasattr(column, 'precision'): self.form_fields['precision'] = column.precision self.form_fields['scale'] = column.scale # Expression column if hasattr(column, 'expression'): self.form_fields['expression'] = column.expression self.form_fields['output_data_type'] = column.output_data_type def bool_to_check(self, state): """ Converts a boolean to a Qt checkstate. :param state: True/False :type state: boolean :rtype: Qt.CheckState """ return Qt.Checked if state else Qt.Unchecked def init_form_fields(self): """ Initializes work area 'form_fields' dictionary with default values. Used when creating a new column. """ none = QApplication.translate('CodeProperty', 'None') self.form_fields['colname'] = '' self.form_fields['value'] = None self.form_fields['mandt'] = False self.form_fields['search'] = False self.form_fields['unique'] = False self.form_fields['index'] = False self.form_fields['minimum'] = self.type_attribs.get('minimum', 0) self.form_fields['maximum'] = self.type_attribs.get('maximum', 0) self.form_fields['srid'] = self.type_attribs.get('srid', "") self.form_fields['geom_type'] = self.type_attribs.get('geom_type', 0) self.form_fields['in_db'] = self.in_db self.form_fields['prefix_source'] = self.type_attribs.get( 'prefix_source', none ) self.form_fields['columns'] = self.type_attribs.get( 'columns', [] ) self.form_fields['column_separators'] = self.type_attribs.get( 'column_separators', [] ) self.form_fields['leading_zero'] = self.type_attribs.get( 'leading_zero', '' ) self.form_fields['separator'] = self.type_attribs.get( 'separator', '' ) self.form_fields['enable_editing'] = self.type_attribs.get( 'enable_editing', '' ) self.form_fields['disable_auto_increment'] = self.type_attribs.get( 'disable_auto_increment', '' ) self.form_fields['hide_prefix'] = self.type_attribs.get( 'hide_prefix', '' ) self.form_fields['precision'] = self.type_attribs.get( 'precision', 18 ) self.form_fields['scale'] = self.type_attribs.get( 'scale', 6 ) self.form_fields['entity_relation'] = \ self.type_attribs['FOREIGN_KEY'].get('entity_relation', None) self.form_fields['entity_relation'] = \ self.type_attribs['LOOKUP'].get('entity_relation', None) self.form_fields['first_parent'] = \ self.type_attribs['MULTIPLE_SELECT'].get('first_parent', None) self.form_fields['second_parent'] = \ self.type_attribs['MULTIPLE_SELECT'].get('second_parent', None) self.form_fields['min_use_current_date'] = \ self.type_attribs['DATE'].get('min_use_current_date', None) self.form_fields['max_use_current_date'] = \ self.type_attribs['DATE'].get('max_use_current_date', None) self.form_fields['min_use_current_datetime'] = \ self.type_attribs['DATETIME'].get('min_use_current_datetime', None) self.form_fields['max_use_current_datetime'] = \ self.type_attribs['DATETIME'].get('max_use_current_datetime', None) self.form_fields['expression'] = self.type_attribs.get( 'expression', '' ) self.form_fields['output_data_type'] = self.type_attribs.get( 'output_data_type', '' ) def init_type_attribs(self): """ Initializes data type attributes. The attributes are used to set the form controls state when a particular data type is selected. mandt - enables/disables checkbox 'Mandatory' search - enables/disables checkbox 'Searchable' unique - enables/disables checkbox 'Unique' index - enables/disables checkbox 'Index' *property - function to execute when a data type is selected. """ self.type_attribs['VARCHAR'] = { 'mandt':{'check_state':False, 'enabled_state':True}, 'search':{'check_state':True, 'enabled_state':True}, 'unique':{'check_state':False, 'enabled_state':True}, 'index':{'check_state':False, 'enabled_state':True}, 'maximum':30,'property': self.varchar_property } self.type_attribs['INT'] = { 'mandt':{'check_state':False, 'enabled_state':True}, 'search':{'check_state':True, 'enabled_state':True}, 'unique':{'check_state':False, 'enabled_state':True}, 'index':{'check_state':False, 'enabled_state':False}, 'minimum':0, 'maximum':0, 'property':self.bigint_property } self.type_attribs['TEXT'] = { 'mandt':{'check_state':False, 'enabled_state':True}, 'search':{'check_state':False, 'enabled_state':False}, 'unique':{'check_state':False, 'enabled_state':False}, 'index':{'check_state':False, 'enabled_state':False}, } self.type_attribs['DOUBLE' ] = { 'mandt':{'check_state':False, 'enabled_state':True}, 'search':{'check_state':True, 'enabled_state':True}, 'unique':{'check_state':False, 'enabled_state':True}, 'index':{'check_state':False, 'enabled_state':True}, 'minimum':0.0, 'maximum':0.0, 'precision': 18, 'scale': 6, 'property':self.double_property } self.type_attribs['DATE'] = { 'mandt':{'check_state':False, 'enabled_state':True}, 'search':{'check_state':False, 'enabled_state':True}, 'unique':{'check_state':False, 'enabled_state':False}, 'index':{'check_state':False, 'enabled_state':False}, 'minimum':datetime.date.min, 'maximum':datetime.date.max, 'min_use_current_date':False, 'max_use_current_date':False, 'property':self.date_property } self.type_attribs['DATETIME'] = { 'mandt':{'check_state':False, 'enabled_state':True}, 'search':{'check_state':False, 'enabled_state':True}, 'unique':{'check_state':False, 'enabled_state':False}, 'index':{'check_state':False, 'enabled_state':False}, 'minimum':datetime.datetime.min, 'maximum':datetime.datetime.max, 'min_use_current_datetime':False, 'max_use_current_datetime':False, 'property':self.dtime_property } self.type_attribs['FOREIGN_KEY'] = { 'mandt':{'check_state':False, 'enabled_state':True}, 'search':{'check_state':False, 'enabled_state':False}, 'unique':{'check_state':False, 'enabled_state':False}, 'index':{'check_state':False, 'enabled_state':False}, 'entity_relation':None, 'show_in_parent': True, 'show_in_child': True, 'property':self.fk_property, 'prop_set':False } self.type_attribs['LOOKUP'] = { 'mandt':{'check_state':False, 'enabled_state':True}, 'search':{'check_state':True, 'enabled_state':True}, 'unique':{'check_state':False, 'enabled_state':False}, 'index':{'check_state':False, 'enabled_state':False}, 'entity_relation':{}, 'property':self.lookup_property, 'prop_set':False } self.type_attribs['GEOMETRY'] = { 'mandt':{'check_state':False, 'enabled_state':False}, 'search':{'check_state':False, 'enabled_state':False}, 'unique':{'check_state':False, 'enabled_state':False}, 'index':{'check_state':False, 'enabled_state':False}, 'srid':"", 'geom_type':0, 'property':self.geometry_property, 'prop_set':False } self.type_attribs['BOOL'] = { 'mandt':{'check_state':False, 'enabled_state':False}, 'search':{'check_state':False, 'enabled_state':False}, 'unique':{'check_state':False, 'enabled_state':False}, 'index':{'check_state':False, 'enabled_state':False} } self.type_attribs['PERCENT'] = { 'mandt': {'check_state': False, 'enabled_state': False}, 'search': {'check_state': False, 'enabled_state': True}, 'unique': {'check_state': False, 'enabled_state': False}, 'index': {'check_state': False, 'enabled_state': False} } self.type_attribs['ADMIN_SPATIAL_UNIT'] = { 'mandt':{'check_state':False, 'enabled_state':True}, 'search':{'check_state':True, 'enabled_state':True}, 'unique':{'check_state':False, 'enabled_state':False}, 'index':{'check_state':False, 'enabled_state':False}, 'entity_relation':None} self.type_attribs['MULTIPLE_SELECT'] = { 'mandt':{'check_state':False, 'enabled_state':True}, 'search':{'check_state':False, 'enabled_state':True}, 'unique':{'check_state':False, 'enabled_state':False}, 'index':{'check_state':False, 'enabled_state':False}, 'first_parent':None, 'second_parent':self.entity, 'property':self.multi_select_property, 'prop_set':False } self.type_attribs['AUTO_GENERATED'] = { 'mandt': {'check_state': False, 'enabled_state': True}, 'search': {'check_state': True, 'enabled_state': True}, 'unique': {'check_state': True, 'enabled_state': True}, 'index': {'check_state': True, 'enabled_state': True}, 'prefix_source': '', 'columns':[], 'column_separators':[], 'leading_zero': '', 'separator':'', 'disable_auto_increment': False, 'enable_editing': False, 'property': self.code_property, 'hide_prefix': False, 'prop_set': True} self.type_attribs['EXPRESSION'] = { 'mandt': {'check_state': False, 'enabled_state': True}, 'search': {'check_state': False, 'enabled_state': True}, 'unique': {'check_state': False, 'enabled_state': True}, 'index': {'check_state': False, 'enabled_state': True}, 'output_data_type': '', 'expression':'', 'property': self.expression_property, 'prop_set': False} def data_type_property(self): """ Executes the function assigned to the property attribute of the current selected data type. """ self.type_attribs[self.current_type_info()]['property']() def varchar_property(self): """ Opens the property editor for the Varchar data type. If successful, set a minimum column in work area 'form fields' """ editor = VarcharProperty(self, self.form_fields) result = editor.exec_() if result == 1: self.form_fields['maximum'] = editor.max_len() def bigint_property(self): """ Opens a property editor for the BigInt data type. """ editor = BigintProperty(self, self.form_fields) result = editor.exec_() if result == 1: self.form_fields['minimum'] = editor.min_val() self.form_fields['maximum'] = editor.max_val() def double_property(self): """ Opens a property editor for the Double data type. """ editor = DoubleProperty(self, self.form_fields) result = editor.exec_() if result == 1: self.form_fields['minimum'] = editor.min_val() self.form_fields['maximum'] = editor.max_val() self.form_fields['precision'] = editor.precision self.form_fields['scale'] = editor.scale def date_property(self): """ Opens a property editor for the Date data type. """ editor = DateProperty(self, self.form_fields) result = editor.exec_() if result == 1: self.form_fields['minimum'] = editor.min_val() self.form_fields['maximum'] = editor.max_val() self.form_fields['min_use_current_date'] = \ editor.min_use_current_date self.form_fields['max_use_current_date'] = \ editor.max_use_current_date def dtime_property(self): """ Opens a property editor for the DateTime data type. """ editor = DTimeProperty(self, self.form_fields) result = editor.exec_() if result == 1: self.form_fields['minimum'] = editor.min_val() self.form_fields['maximum'] = editor.max_val() self.form_fields['min_use_current_datetime'] = \ editor.min_use_current_datetime self.form_fields['max_use_current_datetime'] = \ editor.max_use_current_datetime def geometry_property(self): """ Opens a property editor for the Geometry data type. If successful, set the srid(projection), geom_type (LINE, POLYGON...) and prop_set which is boolean flag to verify that all the geometry properties are set. Constraint - If 'prop_set' is False column cannot be saved. """ editor = GeometryProperty(self, self.form_fields) result = editor.exec_() if result == 1: self.form_fields['srid'] = editor.coord_sys() self.form_fields['geom_type'] = editor.geom_type() self.property_set() def admin_spatial_unit_property(self): """ Sets entity relation property used when creating column of type ADMIN_SPATIAL_UNIT """ er_fields = {} er_fields['parent'] = self.entity er_fields['parent_column'] = None er_fields['display_columns'] = [] er_fields['child'] = None er_fields['child_column'] = None self.form_fields['entity_relation'] = EntityRelation(self.profile, **er_fields) def fk_property(self): """ Opens a property editor for the ForeignKey data type. """ if len(self.edtColName.displayText())==0: self.show_message("Please enter column name!") return # filter list of lookup tables, don't show internal # tables in list of lookups fk_ent = [entity for entity in self.profile.entities.items() \ if entity[1].TYPE_INFO not in self.EX_TYPE_INFO] fk_ent = [entity for entity in fk_ent if unicode(entity[0]) \ not in self.FK_EXCLUDE] relation = {} relation['form_fields'] = self.form_fields relation['fk_entities'] = fk_ent relation['profile'] = self.profile relation['entity'] = self.entity relation['column_name'] = unicode(self.edtColName.text()) relation['show_in_parent'] = '1' relation['show_in_child'] = '1' editor = FKProperty(self, relation) result = editor.exec_() if result == 1: self.form_fields['entity_relation'] = editor.entity_relation() relation['show_in_parent'] = editor.show_in_parent() relation['show_in_child'] = editor.show_in_child() self.property_set() def lookup_property(self): """ Opens a lookup type property editor """ editor = LookupProperty(self, self.form_fields, profile=self.profile) result = editor.exec_() if result == 1: self.form_fields['entity_relation'] = editor.entity_relation() self.property_set() def multi_select_property(self): """ Opens a multi select property editor """ if len(self.edtColName.displayText())==0: self.show_message("Please enter column name!") return editor = MultiSelectProperty(self, self.form_fields, self.entity, self.profile) result = editor.exec_() if result == 1: self.form_fields['first_parent'] = editor.lookup() self.form_fields['second_parent'] = self.entity self.property_set() def code_property(self): """ Opens the code data type property editor """ editor = CodeProperty(self, self.form_fields, entity=self.entity, profile=self.profile) result = editor.exec_() if result == 1: self.form_fields['prefix_source'] = editor.prefix_source() self.form_fields['columns'] = editor.columns() self.form_fields['leading_zero'] = editor.leading_zero() self.form_fields['separator'] = editor.separator() self.form_fields['disable_auto_increment'] = editor.disable_auto_increment() self.form_fields['enable_editing'] = editor.enable_editing() self.form_fields['column_separators'] = editor.column_separators() self.form_fields['hide_prefix'] = editor.hide_prefix() self.property_set() def expression_property(self): """ Opens the code data type property editor """ layer = self.create_layer() editor = ExpressionProperty(layer, self.form_fields, self) result = editor.exec_() if result == 1: self.form_fields['expression'] = editor.expression_text() self.form_fields['output_data_type'] = editor.get_output_data_type() self.property_set() def create_layer(self): srid = None column = '' if self.entity.has_geometry_column(): geom_cols = [col.name for col in self.entity.columns.values() if col.TYPE_INFO == 'GEOMETRY'] column = geom_cols[0] geom_col_obj = self.entity.columns[column] if geom_col_obj.srid >= 100000: srid = geom_col_obj.srid layer = vector_layer(self.entity.name, geom_column=column, proj_wkt=srid) return layer def create_column(self): """ Creates a new BaseColumn. """ column = None if self.type_info <> "": if self.type_info == 'ADMIN_SPATIAL_UNIT': self.admin_spatial_unit_property() column = BaseColumn.registered_types[self.type_info] \ (self.form_fields['colname'], self.entity, **self.form_fields) return column if self.is_property_set(self.type_info): column = BaseColumn.registered_types[self.type_info] \ (self.form_fields['colname'], self.entity, self.form_fields['geom_type'], self.entity, **self.form_fields) else: self.show_message(self.tr('Please set column properties.')) return else: raise self.tr("No type to create.") return column def property_set(self): self.prop_set = True self.type_attribs[self.current_type_info()]['prop_set'] = True def is_property_set(self, ti): """ Checks if column property is set by reading the value of attribute 'prop_set' :param ti: Type info to check for prop set :type ti: BaseColumn.TYPE_INFO :rtype: boolean """ return self.type_attribs[ti].get('prop_set', True) def property_by_name(self, ti, name): try: return self.dtype_property(ti)['property'][name] except: return None def populate_data_type_cbo(self): """ Fills the data type combobox widget with BaseColumn type names. """ self.cboDataType.clear() for name, col in BaseColumn.types_by_display_name().iteritems(): # Specify columns to exclude if col.TYPE_INFO not in self._exclude_col_type_info: self.cboDataType.addItem(name) if self.cboDataType.count() > 0: self.cboDataType.setCurrentIndex(0) def change_data_type(self, index): """ Called by type combobox when you select a different data type. """ text = self.cboDataType.itemText(index) col_cls = BaseColumn.types_by_display_name().get(text, None) if col_cls is None: return ti = col_cls.TYPE_INFO if ti not in self.type_attribs: msg = self.tr('Column type attributes could not be found.') self.notice_bar.clear() self.notice_bar.insertErrorNotification(msg) return self.btnColProp.setEnabled(self.type_attribs[ti].has_key('property')) self.type_info = ti opts = self.type_attribs[ti] self.set_optionals(opts) self.set_min_max_defaults(ti) def set_optionals(self, opts): """ Enable/disables form controls based on selected column data type attributes param opts: Dictionary type properties of selected column type opts: dict """ self.cbMandt.setEnabled(opts['mandt']['enabled_state']) self.cbSearch.setEnabled(opts['search']['enabled_state']) self.cbUnique.setEnabled(opts['unique']['enabled_state']) self.cbIndex.setEnabled(opts['index']['enabled_state']) self.cbMandt.setCheckState(self.bool_to_check(opts['mandt']['check_state'])) self.cbSearch.setCheckState(self.bool_to_check(opts['search']['check_state'])) self.cbUnique.setCheckState(self.bool_to_check(opts['unique']['check_state'])) self.cbIndex.setCheckState(self.bool_to_check(opts['index']['check_state'])) def set_min_max_defaults(self, type_info): """ sets the work area 'form_fields' default values (minimum/maximum) from the column's type attribute dictionary :param type_info: BaseColumn.TYPE_INFO :type type_info: str """ self.form_fields['minimum'] = \ self.type_attribs[type_info].get('minimum', 0) self.form_fields['maximum'] = \ self.type_attribs[type_info].get('maximum', 0) def current_type_info(self): """ Returns a TYPE_INFO of a data type :rtype: str """ text = self.cboDataType.itemText(self.cboDataType.currentIndex()) try: return BaseColumn.types_by_display_name()[text].TYPE_INFO except: return '' def fill_work_area(self): """ Sets work area 'form_fields' with form control values """ self.form_fields['colname'] = unicode(self.edtColName.text()) self.form_fields['description'] = unicode(self.edtColDesc.text()) self.form_fields['label'] = unicode(self.txt_form_label.text()) self.form_fields['index'] = self.cbIndex.isChecked() self.form_fields['mandatory'] = self.cbMandt.isChecked() self.form_fields['searchable'] = self.cbSearch.isChecked() self.form_fields['unique'] = self.cbUnique.isChecked() self.form_fields['user_tip'] = unicode(self.edtUserTip.text()) def show_message(self, message): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle(QApplication.translate("AttributeEditor", "STDM")) msg.setText(message) msg.exec_() def accept(self): col_name = unicode(self.edtColName.text()).strip() # column name is not empty if len(col_name)==0 or col_name == '_': self.show_message(self.tr('Please enter a valid column name.')) return False # check for STDM reserved keywords if col_name in RESERVED_KEYWORDS: self.show_message( self.tr(u"'{0}' is a reserved keyword used internally by STDM.\n"\ "Please choose another column name.".format(col_name)) ) return False new_column = self.make_column() if new_column is None: LOGGER.debug("Error creating column!") self.show_message('Unable to create column!') return False if self.column is None: # new column if self.duplicate_check(col_name): self.show_message(self.tr("Column with the same name already " "exist in this entity!")) return False if self.auto_entity_add: self.entity.add_column(new_column) self.column = new_column self.done(1) else: # editing a column self.column = new_column self.done(1) def cancel(self): self.done(0) def make_column(self): """ Returns a newly created column :rtype: BaseColumn """ self.fill_work_area() col = self.create_column() return col def duplicate_check(self, name): """ Return True if we have a column in the current entity with same name as our new column :param col_name: column name :type col_name: str """ # check if another column with the same name exist in the current entity if name in self.entity.columns: return True else: return False def rejectAct(self): self.done(0)
class OptionsDialog(QDialog, Ui_DlgOptions): """ Dialog for editing STDM settings. """ def __init__(self, iface): QDialog.__init__(self, iface.mainWindow()) self.setupUi(self) self.iface = iface self.notif_bar = NotificationBar(self.vlNotification, 6000) self._apply_btn = self.buttonBox.button(QDialogButtonBox.Apply) self._reg_config = RegistryConfig() self._db_config = DatabaseConfig() version = version_from_metadata() upgrade_label_text = self.label_9.text().replace('1.4', version) self.label_9.setText(upgrade_label_text) #Connect signals self._apply_btn.clicked.connect(self.apply_settings) self.buttonBox.accepted.connect(self.on_accept) self.chk_pg_connections.toggled.connect(self._on_use_pg_connections) self.cbo_pg_connections.currentIndexChanged.connect( self._on_pg_profile_changed) self.btn_db_conn_clear.clicked.connect(self.clear_properties) self.btn_test_db_connection.clicked.connect(self._on_test_connection) self.btn_supporting_docs.clicked.connect( self._on_choose_supporting_docs_path ) self.btn_template_folder.clicked.connect( self._on_choose_doc_designer_template_path ) self.btn_composer_out_folder.clicked.connect( self._on_choose_doc_generator_output_path ) self.upgradeButton.toggled.connect( self.manage_upgrade ) self._config = StdmConfiguration.instance() self._default_style_sheet = self.txtRepoLocation.styleSheet() self.manage_upgrade() self.init_gui() def init_gui(self): #Set integer validator for the port number int_validator = QIntValidator(1024, 49151) self.txtPort.setValidator(int_validator) #Load profiles self.load_profiles() #Set current profile in the combobox curr_profile = current_profile() if not curr_profile is None: setComboCurrentIndexWithText(self.cbo_profiles, curr_profile.name) #Load current database connection properties self._load_db_conn_properties() #Load existing PostgreSQL connections self._load_qgis_pg_connections() #Load directory paths self._load_directory_paths() self.edtEntityRecords.setMaximum(MAX_LIMIT) self.edtEntityRecords.setValue(get_entity_browser_record_limit()) # Debug logging lvl = debug_logging() if lvl: self.chk_logging.setCheckState(Qt.Checked) else: self.chk_logging.setCheckState(Qt.Unchecked) def load_profiles(self): """ Load existing profiles into the combobox. """ profile_names = self._config.profiles.keys() self.cbo_profiles.clear() self.cbo_profiles.addItem('') self.cbo_profiles.addItems(profile_names) def _load_db_conn_properties(self): #Load database connection properties from the registry. db_conn = self._db_config.read() if not db_conn is None: self.txtHost.setText(db_conn.Host) self.txtPort.setText(db_conn.Port) self.txtDatabase.setText(db_conn.Database) def _load_qgis_pg_connections(self): """ Load QGIS postgres connections. """ self.cbo_pg_connections.addItem('') profiles = pg_profile_names() for profile in profiles: self.cbo_pg_connections.addItem(profile[0], profile[1]) def _load_directory_paths(self): #Load paths to various directory settings. comp_out_path = composer_output_path() comp_temp_path = composer_template_path() source_doc_path = source_documents_path() if not source_doc_path is None: self.txtRepoLocation.setText(source_doc_path) if not comp_out_path is None: self.txt_output_dir.setText(comp_out_path) if not comp_temp_path is None: self.txt_template_dir.setText(comp_temp_path) def _on_use_pg_connections(self, state): #Slot raised when to (not) use existing pg connections if not state: self.cbo_pg_connections.setCurrentIndex(0) self.cbo_pg_connections.setEnabled(False) #Restore current connection in registry self._load_db_conn_properties() else: self.cbo_pg_connections.setEnabled(True) def _on_pg_profile_changed(self, index): """ Slot raised when the index of the pg profile changes. If the selection is valid then the system will attempt to extract the database connection properties of the selected profile stored in the registry. """ if index == 0: return profile_path = self.cbo_pg_connections.itemData(index) q_config = QGISRegistryConfig(profile_path) db_items = q_config.read(['Database', 'Host', 'Port']) if len(db_items) > 0: self.txtDatabase.setText(db_items['Database']) self.txtHost.setText(db_items['Host']) self.txtPort.setText(db_items['Port']) def clear_properties(self): """ Clears the host, database name and port number values from the respective controls. """ self.txtDatabase.clear() self.txtHost.clear() self.txtPort.clear() def _on_choose_supporting_docs_path(self): #Slot raised to select directory for supporting documents. self._set_selected_directory(self.txtRepoLocation, self.tr( 'Supporting Documents Directory') ) def _on_choose_doc_designer_template_path(self): #Slot raised to select directory for document designer templates. self._set_selected_directory(self.txt_template_dir, self.tr( 'Document Designer Templates Directory') ) def _on_choose_doc_generator_output_path(self): #Slot raised to select directory for doc generator outputs. self._set_selected_directory(self.txt_output_dir, self.tr( 'Document Generator Output Directory') ) def _set_selected_directory(self, txt_box, title): def_path= txt_box.text() sel_doc_path = QFileDialog.getExistingDirectory(self, title, def_path) if sel_doc_path: normalized_path = QDir.fromNativeSeparators(sel_doc_path) txt_box.clear() txt_box.setText(normalized_path) def _validate_db_props(self): #Test if all properties have been specified status = True self.notif_bar.clear() if not self.txtHost.text(): msg = self.tr('Please specify the database host address.') self.notif_bar.insertErrorNotification(msg) status = False if not self.txtPort.text(): msg = self.tr('Please specify the port number.') self.notif_bar.insertErrorNotification(msg) status = False if not self.txtDatabase.text(): msg = self.tr('Please specify the database name.') self.notif_bar.insertErrorNotification(msg) status = False return status def _database_connection(self): #Creates a databaase connection object from the specified args host = self.txtHost.text() port = self.txtPort.text() database = self.txtDatabase.text() #Create database connection object db_conn = DatabaseConnection(host, port, database) return db_conn def _on_test_connection(self): """ Slot raised to test database connection. """ status = self._validate_db_props() if not status: return login_dlg = loginDlg(self, True) db_conn = self._database_connection() login_dlg.set_database_connection(db_conn) res = login_dlg.exec_() if res == QDialog.Accepted: msg = self.tr(u"Connection to '{0}' database was " "successful.".format(db_conn.Database)) QMessageBox.information(self, self.tr('Database Connection'), msg) def set_current_profile(self): """ Saves the given profile name as the current profile. """ profile_name = self.cbo_profiles.currentText() if not profile_name: self.notif_bar.clear() msg = self.tr('Profile name is empty, current profile will not ' 'be set.') self.notif_bar.insertErrorNotification(msg) return False save_current_profile(profile_name) return True def save_database_properties(self): """ Saves the specified database connection properties to the registry. :return: True if the connection properties were successfully saved. :rtype: bool """ if not self._validate_db_props(): return False #Create a database object and write it to the registry db_conn = self._database_connection() self._db_config.write(db_conn) return True def set_supporting_documents_path(self): """ Set the directory of supporting documents. :return: True if the directory was set in the registry, otherwise False. :rtype: bool """ path = self.txtRepoLocation.text() if not path: msg = self.tr('Please set the supporting documents directory.') self.notif_bar.insertErrorNotification(msg) return False #Validate path if not self._check_path_exists(path, self.txtRepoLocation): return False #Commit to registry self._reg_config.write({NETWORK_DOC_RESOURCE: path}) return True def set_document_templates_path(self): """ Set the directory of document designer templates. :return: True if the directory was set in the registry, otherwise False. :rtype: bool """ path = self.txt_template_dir.text() if not path: msg = self.tr('Please set the document designer templates ' 'directory.') self.notif_bar.insertErrorNotification(msg) return False #Validate path if not self._check_path_exists(path, self.txt_template_dir): return False #Commit to registry self._reg_config.write({COMPOSER_TEMPLATE: path}) return True def set_document_output_path(self): """ Set the directory of document generator outputs. :return: True if the directory was set in the registry, otherwise False. :rtype: bool """ path = self.txt_output_dir.text() if not path: msg = self.tr('Please set the document generator output directory' '.') self.notif_bar.insertErrorNotification(msg) return False #Validate path if not self._check_path_exists(path, self.txt_output_dir): return False #Commit to registry self._reg_config.write({COMPOSER_OUTPUT: path}) return True def _check_path_exists(self, path, text_box): #Validates if the specified folder exists dir = QDir() if not dir.exists(path): msg = self.tr(u"'{0}' directory does not exist.".format(path)) self.notif_bar.insertErrorNotification(msg) #Highlight textbox control text_box.setStyleSheet(INVALIDATESTYLESHEET) timer = QTimer(self) #Sync interval with that of the notification bar timer.setInterval(self.notif_bar.interval) timer.setSingleShot(True) #Remove previous connected slots (if any) receivers = timer.receivers(SIGNAL('timeout()')) if receivers > 0: self._timer.timeout.disconnect() timer.start() timer.timeout.connect(lambda:self._restore_stylesheet( text_box) ) return False return True def _restore_stylesheet(self, textbox): # Slot raised to restore the original stylesheet of the textbox control textbox.setStyleSheet(self._default_style_sheet) # Get reference to timer and delete sender = self.sender() if not sender is None: sender.deleteLater() def apply_debug_logging(self): # Save debug logging logger = logging.getLogger('stdm') if self.chk_logging.checkState() == Qt.Checked: logger.setLevel(logging.DEBUG) set_debug_logging(True) else: logger.setLevel(logging.ERROR) set_debug_logging(False) def apply_settings(self): """ Save settings. :return: True if the settings were successfully applied, otherwise False. :rtype: bool """ #Set current profile if not self.set_current_profile(): return False #Set db connection properties if not self.save_database_properties(): return False #Set supporting documents directory if not self.set_supporting_documents_path(): return False #Set document designer templates path if not self.set_document_templates_path(): return False #Set document generator output path if not self.set_document_output_path(): return False self.apply_debug_logging() # Set Entity browser record limit save_entity_browser_record_limit(self.edtEntityRecords.value()) msg = self.tr('Settings successfully saved.') self.notif_bar.insertSuccessNotification(msg) return True def on_accept(self): """ Slot raised to save the settings of the current widget and close the widget. """ if not self.apply_settings(): return self.accept() def manage_upgrade(self): """ A slot raised when the upgrade button is clicked. It disables or enables the upgrade button based on the ConfigUpdated registry value. """ self.config_updated_dic = self._reg_config.read( [CONFIG_UPDATED] ) # if config file exists, check if registry key exists if len(self.config_updated_dic) > 0: config_updated_val = self.config_updated_dic[ CONFIG_UPDATED ] # If failed to upgrade, enable the upgrade button if config_updated_val == '0' or config_updated_val == '-1': self.upgradeButton.setEnabled(True) # disable the button if any other value. else: self.upgradeButton.setEnabled(False) else: self.upgradeButton.setEnabled(False)
class SourceDocumentTranslatorDialog(QDialog, Ui_SourceDocumentTranslatorDialog, TranslatorDialogBase): """ Dialog for defining configuration settings for the SourceDocumentTranslator implementation. """ def __init__(self, parent, source_cols, dest_table, dest_col, src_col): QDialog.__init__(self, parent) self.setupUi(self) TranslatorDialogBase.__init__(self, source_cols, dest_table, dest_col, src_col) self._notif_bar = NotificationBar(self.vlNotification) #Assert if the entity supports documents self._assert_entity_supports_documents() #Set the source document directory self.source_document_directory = None #Document type name self._document_type_name = self._dest_col #Document type ID self._document_type_id = None #Set document type ID self._set_document_type_id() #Connect slots self.btn_source_doc_folder.clicked.connect( self._load_source_document_directory_selector) @property def document_type_id(self): """ :return: Returns source document type id. Otherwise None if not set. :rtype: int """ return self._document_type_id @property def document_type(self): """ :return: Returns the name of the selected document type. :rtype: str """ return self._document_type_name @property def documents_directory(self): """ :return: Returns the specified root directory for supporting documents. Otherwise None if not specified by the user. :rtype: str """ return self.txtRootFolder.text() def _assert_entity_supports_documents(self): #Check if the entity supports documents and close automatically if not. entity = self.entity() if entity is None: msg = self.tr('Invalid table object') raise RuntimeError(msg) if not entity.supports_documents: msg = self.tr('The selected destination table does not support ' 'documents.\nHence, this translator is not ' 'applicable.') raise RuntimeError(msg) def _set_document_type_id(self): #Load document id based on the name entity = self.entity() if entity is None: return vl_cls = entity_model(entity.supporting_doc.document_type_entity, entity_only=True) vl_obj = vl_cls() res = vl_obj.queryObject().all() for r in res: if r.value == self._dest_col: self._document_type_id = r.id break if not entity.supports_documents: msg = self.tr('The selected column does not correspond to a ' 'document type.\nHence, this translator is not ' 'applicable.') title = self.tr('Invalid Document Type') QMessageBox.critical(self, title, msg) #Close dialog self.reject() def _load_source_document_directory_selector(self): #Load file dialog for selecting source documents directory title = self.tr('Select Source Document Directory') def_path = self.txtRootFolder.text() #Use the last set source document directory if not def_path: def_path = last_document_path() sel_doc_path = QFileDialog.getExistingDirectory(self, title, def_path) if sel_doc_path: normalized_path = QDir.fromNativeSeparators(sel_doc_path) self.txtRootFolder.clear() self.txtRootFolder.setText(normalized_path) def value_translator(self): source_doc_translator = SourceDocumentTranslator() source_doc_translator.set_referencing_table(self._dest_table) source_doc_translator.set_referencing_column(self._dest_col) #Just use the source column for getting the relative image path # and name source_doc_translator.add_source_reference_column( self._src_col, self._dest_col) source_doc_translator.entity = self.entity() source_doc_translator.document_type_id = self._document_type_id source_doc_translator.document_type = self._document_type_name source_doc_translator.source_directory = self.documents_directory return source_doc_translator def validate(self): """ :return: Return True if the source document directory exists, otherwise False. :rtype: bool """ source_doc_path = self.txtRootFolder.text() #Clear previous notifications self._notif_bar.clear() if not source_doc_path: msg = self.tr('Please set the root directory of source documents.') self._notif_bar.insertErrorNotification(msg) return False dir = QDir() if not dir.exists(source_doc_path): msg = self.tr( u"'{0}' directory does not exist.".format(source_doc_path)) self._notif_bar.insertErrorNotification(msg) return False return True def accept(self): """ Validate before accepting user input. """ if self.validate(): super(SourceDocumentTranslatorDialog, self).accept()
class ComposerChartConfigEditor(WIDGET, BASE): def __init__(self, composer_wrapper, parent=None): QWidget.__init__(self, parent) self.setupUi(self) self._composer_wrapper = composer_wrapper self._notif_bar = NotificationBar(self.vl_notification) self.cbo_chart_type.currentIndexChanged[int].connect(self._on_chart_type_changed) ''' Register chartname to the positional index of the corresponding editor ''' self._short_name_idx = {} # Add registered chart types self._load_chart_type_settings() # Load legend positions self._load_legend_positions() self.groupBox_2.setCollapsed(True) self.groupBox_2.collapsedStateChanged.connect(self._on_series_properties_collapsed) # Load fields if the data source has been specified ds_name = self._composer_wrapper.selectedDataSource() self.ref_table.load_data_source_fields(ds_name) # Load referenced table list self.ref_table.load_link_tables() # Connect signals self._composer_wrapper.dataSourceSelected.connect(self.ref_table.on_data_source_changed) self.ref_table.referenced_table_changed.connect(self.on_referenced_table_changed) def _load_legend_positions(self): from stdm.composer import legend_positions self.cbo_legend_pos.clear() for k, v in legend_positions.items(): self.cbo_legend_pos.addItem(k, v) # Select 'Automatic' option setComboCurrentIndexWithText(self.cbo_legend_pos, QApplication.translate("ChartConfiguration", "Automatic")) def _load_chart_type_settings(self): for cts in ChartTypeUISettings.registry: self.add_chart_type_setting(cts) def add_chart_type_setting(self, cts): """ Adds a chart type and corresponding series properties editor to the collection. :param cts: Chart type setting containing a display name and corresponding editor. :type cts: ChartTypeUISettings """ cts_obj = cts(self) widget_idx = self.series_type_container.addWidget(cts_obj.editor()) self.cbo_chart_type.insertItem(widget_idx, cts_obj.icon(), cts_obj.title()) # Register short_name index if cts_obj.short_name(): self._short_name_idx[cts_obj.short_name()] = widget_idx def _on_series_properties_collapsed(self, state): """ Slot to check whether the user has specified a chart type and collapse if none has been selected. :param state: True if group box is collapsed, false if not. :type state: bool """ if not state and not self.cbo_chart_type.currentText(): self._notif_bar.clear() msg = QApplication.translate("ComposerChartConfigEditor", "Please select a chart type from " "the drop down list below") self._notif_bar.insertWarningNotification(msg) self.groupBox_2.setCollapsed(True) def _on_chart_type_changed(self, index): """ Slot raised when the chart type selection changes. :param index: Index of the chart type in the combobox :type index: int """ if index >= 0: self.series_type_container.setCurrentIndex(index) def on_referenced_table_changed(self, table): """ Slot raised when the referenced table name changes. This notifies series properties editors of the need to update the fields. :param table: Current table name. :type table: str """ curr_editor = self.series_type_container.currentWidget() if not curr_editor is None: if isinstance(curr_editor, DataSourceNotifier): curr_editor.on_table_name_changed(table) def configuration(self): # Return chart configuration settings config = None curr_editor = self.series_type_container.currentWidget() if not curr_editor is None: try: config = curr_editor.configuration() except AttributeError: raise AttributeError(QApplication.translate("ComposerChartConfigEditor", "Series editor does not contain a method for " "returning a ChartConfigurationSettings object.")) else: raise Exception(QApplication.translate("ComposerChartConfigEditor", "No series editor found.")) if not config is None: ref_table_config = self.ref_table.properties() config.extract_from_linked_table_properties(ref_table_config) config.set_insert_legend(self.gb_legend.isChecked()) config.set_title(self.txt_plot_title.text()) config.set_legend_position(self.cbo_legend_pos.itemData (self.cbo_legend_pos.currentIndex())) return config def composer_item(self): return self._picture_item def _set_graph_properties(self, config): # Set the general graph properties from the config object self.txt_plot_title.setText(config.title()) self.gb_legend.setChecked(config.insert_legend()) setComboCurrentIndexWithItemData(self.cbo_legend_pos, config.legend_position()) def set_configuration(self, configuration): # Load configuration settings short_name = configuration.plot_type if short_name: if short_name in self._short_name_idx: plot_type_idx = self._short_name_idx[short_name] self.cbo_chart_type.setCurrentIndex(plot_type_idx) # Set linked table properties self.ref_table.set_properties(configuration.linked_table_props()) # Set series editor properties curr_editor = self.series_type_container.currentWidget() if not curr_editor is None: try: curr_editor.set_configuration(configuration) self._set_graph_properties(configuration) except AttributeError: msg = QApplication.translate("ComposerChartConfigEditor", "Configuration could not be set for series editor.") self._notif_bar.clear() self._notif_bar.insertErrorNotification(msg) else: msg = QApplication.translate("ComposerChartConfigEditor", "Configuration failed to load. Plot type cannot be determined.") self._notif_bar.clear() self._notif_bar.insertErrorNotification(msg)
class LookupEditor(QDialog, Ui_dlgLookup): """ Form to add/edit lookup entities. """ def __init__(self, parent, profile, lookup=None): """ :param parent: Owner of this dialog :type parent: QWidget :param profile: A profile to add/edit lookup :type profile: Profile :type inplace: Flag to check if lookup creation is initiated from the 'normal' lookup creation process -inplace = False, this is the normal state. If 'inplace' = True, then creation is initiated from the the lookup selection dialog :param lookup: Value list to create, if None this is a new value list else its an edit :type lookup: ValueList """ QDialog.__init__(self, parent) self.setupUi(self) self.profile = profile self.lookup = lookup self.notice_bar = NotificationBar(self.notif_bar) self.init_gui() def init_gui(self): """ Initializes form widgets """ self.edtName.setFocus() if self.lookup: self.edtName.setText( self.lookup.short_name.replace('check_','')) self.edtName.setEnabled(not self.lookup.entity_in_database) self.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(not self.lookup.entity_in_database) self.edtName.textChanged.connect(self.validate_text) def show_notification(self, message): """ Shows a warning notification bar message. :param message: The message of the notification. :type message: String """ msg = self.tr(message) self.notice_bar.clear() self.notice_bar.insertErrorNotification(msg) def validate_text(self, text): """ Validates and updates the entered text if necessary. Spaces are replaced by _ and capital letters are replaced by small. :param text: The text entered :type text: String """ text_edit = self.sender() cursor_position = text_edit.cursorPosition() text_edit.setValidator(None) if len(text) == 0: return locale = QSettings().value("locale/userLocale")[0:2] last_character = text[-1:] if locale == 'en': name_regex = QtCore.QRegExp('^(?=.{0,40}$)[ _a-zA-Z][a-zA-Z0-9_ ]*$') name_validator = QtGui.QRegExpValidator(name_regex) text_edit.setValidator(name_validator) QApplication.processEvents() state = name_validator.validate(text, text.index(last_character))[0] if state != QValidator.Acceptable: self.show_notification(u'"{}" is not allowed at this position.'. format(last_character) ) text = text[:-1] # fix caps, underscores, and spaces if last_character.isupper(): text = text.lower() if last_character == ' ': text = text.replace(' ', '_') if len(text) > 1: if text[0] == ' ' or text[0] == '_': text = text[1:] text = text.replace(' ', '_').lower() self.blockSignals(True) text_edit.setText(text) text_edit.setCursorPosition(cursor_position) self.blockSignals(False) text_edit.setValidator(None) def format_lookup_name(self, name): """ Replace spaces with underscore in a name string :param name: Name to replace spaces :type name: str :rtype: str """ formatted_name = str(name).strip() formatted_name = formatted_name.replace(' ', "_") return formatted_name.lower() def create_lookup(self, name): """ Creates a lookup entity and add it to a profile. If this is an edit, first the previous lookup is removed before adding a new one. :param name: Name of the new/edited lookup :type name: Unicode """ name = self.format_lookup_name(name) new_lookup = self.profile.create_entity(name, value_list_factory) return new_lookup def accept(self): if self.edtName.text() == '' or self.edtName.text() == '_': self.error_message( QApplication.translate( "LookupEditor","Please enter a valid lookup name." ) ) return if self.edtName.text() == 'check': self.error_message(QApplication.translate("LookupEditor", "'check' is used internally by STDM! " "Select another name for the lookup")) return short_name = unicode(self.edtName.text()) if self.lookup is None: # new lookup if self.duplicate_check(short_name): self.show_message( self.tr( "Lookup with the same name already " "exist in the current profile!" ) ) return else: new_lookup = self.create_lookup(short_name) self.profile.add_entity(new_lookup) self.lookup = new_lookup else: self.edit_lookup(short_name) self.done(1) def duplicate_check(self, name): """ Return True if we have an entity in the current profile with same 'name' :param name: entity short_name :type name: Unicode :rtype:boolean """ return self.profile.entities.has_key(name) def edit_lookup(self, short_name): short_name = short_name.replace('check_','') self.lookup.short_name = u'{0}_{1}'.format('check', short_name) def reject(self): self.done(0) def error_message(self, message): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("STDM") msg.setText(message) msg.exec_()
class TemplateDocumentSelector(WIDGET, BASE): """ Dialog for selecting a document template from the saved list. """ def __init__(self, parent=None, selectMode=True, filter_data_source='', access_templates=None): QDialog.__init__(self, parent) self.setupUi(self) self.notifBar = NotificationBar(self.vlNotification) self._mode = selectMode # Filter templates by the specified table name self._filter_data_source = filter_data_source # Document templates in current profile self._profile_templates = [] self._current_profile = current_profile() # Load current profile templates self._load_current_profile_templates() self.access_templates = access_templates or [] if selectMode: self.buttonBox.setVisible(True) self.manageButtonBox.setVisible(False) currHeight = self.size().height() self.resize(200, currHeight) else: self.buttonBox.setVisible(False) self.manageButtonBox.setVisible(True) self.setWindowTitle( QApplication.translate("TemplateDocumentSelector", "Template Manager")) # Configure manage buttons btnEdit = self.manageButtonBox.button(QDialogButtonBox.Ok) btnEdit.setText( QApplication.translate("TemplateDocumentSelector", "Edit...")) btnEdit.setIcon(GuiUtils.get_icon("edit.png")) btnDelete = self.manageButtonBox.button(QDialogButtonBox.Save) btnDelete.setText( QApplication.translate("TemplateDocumentSelector", "Delete")) btnDelete.setIcon(GuiUtils.get_icon("delete.png")) # Connect signals self.buttonBox.accepted.connect(self.onAccept) btnEdit.clicked.connect(self.onEditTemplate) btnDelete.clicked.connect(self.onDeleteTemplate) # Get saved document templates then add to the model templates = documentTemplates() self._docItemModel = QStandardItemModel(parent) self._docItemModel.setColumnCount(2) # Append current profile templates to the model. for dt in self._profile_templates: if self._template_contains_filter_table( dt): # and dt.name in self.access_templates: doc_name_item = self._createDocNameItem(dt.name) file_path_item = QStandardItem(dt.path) self._docItemModel.appendRow([doc_name_item, file_path_item]) self.lstDocs.setModel(self._docItemModel) def _load_current_profile_templates(self): # Loads only those templates that refer to tables in the current # profile. if self._current_profile is None: return # Get saved document templates then add to the model templates = documentTemplates() profile_tables = self._current_profile.table_names() # Get templates for the current profile for name, path in templates.items(): doc_temp = DocumentTemplate.build_from_path(name, path) if doc_temp.data_source is None: continue # Assert data source is in the current profile if doc_temp.data_source.referenced_table_name in profile_tables or \ doc_temp.data_source.name() in user_non_profile_views(): self._add_doc_temp(doc_temp) # self._profile_templates.append(doc_temp) def _add_doc_temp(self, doc_temp): found = False for template in self._profile_templates: if template.name == doc_temp.name: found = True break if not found: self._profile_templates.append(doc_temp) def _template_contains_filter_table(self, document_template): # Returns true if the template refers to the filter data source # If no filter data source defined then always return True if document_template.data_source._dataSourceName in user_non_profile_views( ): return True if not self._filter_data_source: return True referenced_table = document_template.referenced_table_name if referenced_table == self._filter_data_source: return True return False @property def mode(self): return self._mode @property def filter_data_source(self): return self._filter_data_source def _createDocNameItem(self, docName): """ Create a template document standard item. """ # Set icon icon = QIcon() icon.addPixmap(GuiUtils.get_icon_pixmap("document.png"), QIcon.Normal, QIcon.Off) dnItem = QStandardItem(icon, docName) return dnItem def onEditTemplate(self): """ Slot raised to edit document template. """ self.notifBar.clear() if self.documentMapping() is None: self.notifBar.insertErrorNotification(QApplication.translate("TemplateDocumentSelector", \ "Please select a document template to edit")) return templateName, filePath = self.documentMapping() docName, ok = QInputDialog.getText(self, \ QApplication.translate("TemplateDocumentSelector", "Edit Template"), \ QApplication.translate("TemplateDocumentSelector", "Please enter the new template name below"), \ text=templateName) if ok and docName == "": self.notifBar.insertErrorNotification(QApplication.translate("TemplateDocumentSelector", \ "Template name cannot be empty")) return elif docName == templateName: return elif ok and docName != "": result, newTemplatePath = self._editTemplate(filePath, docName) if result: # Update view mIndices = self._selectedMappings() docNameItem = self._docItemModel.itemFromIndex(mIndices[0]) filePathItem = self._docItemModel.itemFromIndex(mIndices[1]) docNameItem.setText(docName) filePathItem.setText(newTemplatePath) self.notifBar.insertSuccessNotification(QApplication.translate("TemplateDocumentSelector", \ "'{0}' template has been successfully updated".format( docName))) else: self.notifBar.insertErrorNotification(QApplication.translate("TemplateDocumentSelector", \ "Error: '{0}' template could not be updated".format( templateName))) def onDeleteTemplate(self): """ Slot raised to delete document template. """ self.notifBar.clear() if self.documentMapping() == None: self.notifBar.insertErrorNotification(QApplication.translate("TemplateDocumentSelector", \ "Please select a document template to delete")) return templateName, filePath = self.documentMapping() result = QMessageBox.warning(self, QApplication.translate("TemplateDocumentSelector", \ "Confirm delete"), QApplication.translate("TemplateDocumentSelector", \ "Are you sure you want to delete '{0}' template?" \ "This action cannot be undone.\nClick Yes to proceed " \ "or No to cancel.".format(templateName)), QMessageBox.Yes | QMessageBox.No) if result == QMessageBox.No: return status = self._deleteDocument(filePath) if status: # Remove item from list using model index row number selectedDocNameIndices = self.lstDocs.selectionModel( ).selectedRows(0) row = selectedDocNameIndices[0].row() self._docItemModel.removeRow(row) self.notifBar.insertSuccessNotification(QApplication.translate("TemplateDocumentSelector", \ "'{0}' template has been successfully removed".format( templateName))) else: self.notifBar.insertErrorNotification(QApplication.translate("TemplateDocumentSelector", \ "Error: '{0}' template could not be removed".format( templateName))) def onAccept(self): """ Slot raised to close the dialog only when a selection has been made by the user. """ self.notifBar.clear() if self.documentMapping() == None: self.notifBar.insertErrorNotification(QApplication.translate("TemplateDocumentSelector", \ "Please select a document")) return self.accept() def _selectedMappings(self): """ Returns the model indices for the selected row. """ selectedDocNameIndices = self.lstDocs.selectionModel().selectedRows(0) selectedFilePathIndices = self.lstDocs.selectionModel().selectedRows(1) if len(selectedDocNameIndices) == 0: return None docNameIndex = selectedDocNameIndices[0] filePathIndex = selectedFilePathIndices[0] return (docNameIndex, filePathIndex) def documentMapping(self): """ Returns a tuple containing the selected document name and the corresponding file name. """ mIndices = self._selectedMappings() if mIndices == None: return None docNameItem = self._docItemModel.itemFromIndex(mIndices[0]) filePathItem = self._docItemModel.itemFromIndex(mIndices[1]) return (docNameItem.text(), filePathItem.text()) def _editTemplate(self, templatePath, newName): """ Updates the template document to use the new name. """ templateFile = QFile(templatePath) if not templateFile.open(QIODevice.ReadOnly): QMessageBox.critical(self, QApplication.translate("TemplateDocumentSelector", "Open Operation Error"), \ "{0}\n{1}".format( QApplication.translate("TemplateDocumentSelector", "Cannot read template file."), \ templateFile.errorString() )) return (False, "") templateDoc = QDomDocument() if templateDoc.setContent(templateFile): composerElement = templateDoc.documentElement() titleAttr = composerElement.attributeNode("_title") if not titleAttr.isNull(): titleAttr.setValue(newName) # Try remove file status = templateFile.remove() if not status: return (False, "") # Create new file newTemplatePath = self._composerTemplatesPath( ) + "/" + newName + ".sdt" newTemplateFile = QFile(newTemplatePath) if not newTemplateFile.open(QIODevice.WriteOnly): QMessageBox.critical(self, QApplication.translate("TemplateDocumentSelector", "Save Operation Error"), \ "{0}\n{1}".format(QApplication.translate("TemplateDocumentSelector", "Could not save template file."), \ newTemplateFile.errorString() )) return (False, "") if newTemplateFile.write(templateDoc.toByteArray()) == -1: QMessageBox.critical(self, QApplication.translate("TemplateDocumentSelector", "Save Error"), \ QApplication.translate("TemplateDocumentSelector", "Could not save template file.")) return (False, "") newTemplateFile.close() return (True, newTemplatePath) def _deleteDocument(self, templatePath): """ Delete the document template from the file system. """ docFile = QFile(templatePath) return docFile.remove() def _composerTemplatesPath(self): """ Reads the path of composer templates in the registry. """ regConfig = RegistryConfig() keyName = "ComposerTemplates" valueCollection = regConfig.read([keyName]) if len(valueCollection) == 0: return None else: return valueCollection[keyName]
class ImageExportSettings(QDialog, Ui_ImageExportSettings): """A dialog for settings options for exporting an image.""" def __init__(self, parent=None, **kwargs): super(ImageExportSettings, self).__init__(parent) self.setupUi(self) self._image_tr = self.tr('Image') self.notif_bar = NotificationBar(self.vl_notification, 6000) #Connect signals self.btn_path.clicked.connect(self._on_choose_image_path) self.buttonBox.accepted.connect(self.on_accept) self.sb_resolution.valueChanged.connect(self._on_resolution_changed) #Set color button defaults self._default_color = Qt.white self.btn_color.setDefaultColor(self._default_color) self.btn_color.setColor(self._default_color) self.btn_color.setAllowAlpha(True) self.path = kwargs.get('image_path', '') self.resolution = kwargs.get('resolution', '96') self.background_color = kwargs.get('background', Qt.transparent) self._update_controls() def _update_controls(self): #Update input controls with export settings' values if self.background_color == Qt.transparent: self.rb_transparent.setChecked(True) self.btn_color.setVisible(False) else: self.rb_fill.setChecked(True) self.btn_color.setColor(self.background_color) self.sb_resolution.setValue(int(self.resolution)) self.txt_path.setText(self.path) #Set image size just in case value does not change self._set_image_size() def _on_resolution_changed(self, value): #Slot raised when the resolution changes self._set_image_size() def _update_export_vars(self): #Update export variables based on user values. self.path = self.txt_path.text() self.resolution = self.sb_resolution.value() if self.rb_transparent.isChecked(): self.background_color = Qt.transparent else: self.background_color = self.btn_color.color() def _set_image_size(self): #Set image size based on the resolution using A4 paper size res = self.sb_resolution.value() if res == 0: return #To mm res_mm = res / 25.4 #A4 landscape size width = int(297 * res_mm) height = int(210 * res_mm) units = 'px' width_display = u'{0} {1}'.format(width, units) height_display = u'{0} {1}'.format(height, units) self.lbl_width.setText(width_display) self.lbl_height.setText(height_display) def _image_filters(self): #Return supported image formats for use in a QFileDialog filter formats = [] for f in QImageWriter.supportedImageFormats(): f_type = f.data() filter_format = u'{0} {1} (*.{2})'.format( f_type.upper(), self._image_tr, f_type ) formats.append(filter_format) return ';;'.join(formats) def _on_choose_image_path(self): #Slot raised to choose image path img_path = self.txt_path.text() title = self.tr('Specify image location') sel_image_path = QFileDialog.getSaveFileName( self, title, img_path, self._image_filters() ) if sel_image_path: self.txt_path.setText(sel_image_path) def on_accept(self): """ Slot raised to save the settings and close the dialog. """ if not self.txt_path.text(): msg = self.tr('Please specify the image path.') self.notif_bar.insertErrorNotification(msg) self.txt_path.setFocus() return self._update_export_vars() self.accept()
class ComposerTableDataSourceEditor(WIDGET, BASE): def __init__(self, composer_wrapper, frame_item, parent=None): QWidget.__init__(self, parent) self.setupUi(self) self._composer_wrapper = composer_wrapper if isinstance(frame_item, QgsComposerFrame): self._composer_table_item = frame_item.multiFrame() else: self._composer_table_item = frame_item self._notif_bar = NotificationBar(self.vl_notification) # Load fields if the data source has been specified ds_name = self._composer_wrapper.selectedDataSource() self.ref_table.load_data_source_fields(ds_name) # Load source tables self.ref_table.load_link_tables() # Connect signals self._composer_wrapper.dataSourceSelected.connect(self.ref_table.on_data_source_changed) # self.ref_table.cbo_ref_table.currentIndexChanged[str].connect(self.set_table_vector_layer) def composer_item(self): return self._composer_table_item def set_table_vector_layer(self, table_name): """ Creates a vector layer and appends it to the composer table item. :param table_name: Name of the linked table containing tabular information. :type table_name: str """ self._notif_bar.clear() if not table_name: return v_layer = vector_layer(table_name) if v_layer is None: msg = QApplication.translate("ComposerTableDataSourceEditor", "A vector layer could not be created from the table.") self._notif_bar.insertErrorNotification(msg) return if not v_layer.isValid(): msg = QApplication.translate("ComposerTableDataSourceEditor", "Invalid vector layer, the table will not be added.") self._notif_bar.insertErrorNotification(msg) return # No need to add the layer in the legend QgsMapLayerRegistry.instance().addMapLayer(v_layer, False) if len(self.composer_item().columns()) > 0: self._composer_table_item.setVectorLayer(v_layer) # _composer_table_item is QgsComposerAttributeTable self._composer_table_item.update() def configuration(self): from stdm.stdm.composer import TableConfiguration linked_table_props = self.ref_table.properties() table_config = TableConfiguration() table_config.set_linked_table(linked_table_props.linked_table) table_config.set_source_field(linked_table_props.source_field) table_config.set_linked_column(linked_table_props.linked_field) return table_config def set_configuration(self, configuration): # Load referenced table editor with item configuration settings. table_props = LinkedTableProps(linked_table=configuration.linked_table(), source_field=configuration.source_field(), linked_field=configuration.linked_field()) self.ref_table.set_properties(table_props)
class OptionsDialog(QDialog, Ui_DlgOptions): """ Dialog for editing STDM settings. """ def __init__(self, iface): QDialog.__init__(self, iface.mainWindow()) self.setupUi(self) self.iface = iface self.notif_bar = NotificationBar(self.vlNotification, 6000) self._apply_btn = self.buttonBox.button(QDialogButtonBox.Apply) self._reg_config = RegistryConfig() self._db_config = DatabaseConfig() # Connect signals self._apply_btn.clicked.connect(self.apply_settings) self.buttonBox.accepted.connect(self.on_accept) self.chk_pg_connections.toggled.connect(self._on_use_pg_connections) self.cbo_pg_connections.currentIndexChanged.connect( self._on_pg_profile_changed) self.btn_db_conn_clear.clicked.connect(self.clear_properties) self.btn_test_db_connection.clicked.connect( self._on_test_db_connection) self.btn_template_folder.clicked.connect( self._on_choose_doc_designer_template_path) self.btn_composer_out_folder.clicked.connect( self._on_choose_doc_generator_output_path) self.btn_test_docs_repo_conn.clicked.connect( self._on_test_cmis_connection) self.txt_atom_pub_url.textChanged.connect(self._on_cmis_url_changed) self.btn_holders_conf_file.clicked.connect( self._on_choose_holders_config_file) self._config = StdmConfiguration.instance() self.init_gui() def init_gui(self): # Set integer validator for the port number int_validator = QIntValidator(1024, 49151) self.txtPort.setValidator(int_validator) # Load profiles self.load_profiles() # Set current profile in the combobox curr_profile = current_profile() if not curr_profile is None: setComboCurrentIndexWithText(self.cbo_profiles, curr_profile.name) # Load current database connection properties self._load_db_conn_properties() # Load existing PostgreSQL connections self._load_qgis_pg_connections() # Load directory paths self._load_directory_paths() # Load document repository-related settings self._load_cmis_config() # Load holders configuration file path self._load_holders_configuration_file() # Debug logging lvl = debug_logging() if lvl: self.chk_logging.setCheckState(Qt.Checked) else: self.chk_logging.setCheckState(Qt.Unchecked) # Shortcut dialog self._check_shortcuts_settings() self.chk_shortcut_dlg.toggled.connect(self._on_check_shortcut_checkbox) def _load_cmis_config(self): """ Load configuration names and IDs in the combobox then select the user specified ID. """ # Load CMIS atom pub URL self.txt_atom_pub_url.setText(cmis_atom_pub_url()) self.cbo_auth_config_name.clear() self.cbo_auth_config_name.addItem('') config_ids = config_entries() for ci in config_ids: id = ci[0] name = ci[1] display = u'{0} ({1})'.format(name, id) self.cbo_auth_config_name.addItem(display, id) # Then select the user specific config ID if specified conf_id = cmis_auth_config_id() if conf_id: id_idx = self.cbo_auth_config_name.findData(conf_id) if id_idx != -1: self.cbo_auth_config_name.setCurrentIndex(id_idx) def _load_holders_configuration_file(self): # Load the path of the holders configuration file. holders_config = holders_config_path() if not holders_config: # Use the default path holders_config = DEF_HOLDERS_CONFIG_PATH self.txt_holders_config.setText(holders_config) def _on_choose_holders_config_file(self): # Slot raised to browse the holders config file holders_config = self.txt_holders_config.text() if not holders_config: holders_config = QDir.home().path() conf_file = QFileDialog.getOpenFileName( self, self.tr('Browse Holders Configuration File'), holders_config, 'Holders INI file (*.ini)') if conf_file: self.txt_holders_config.setText(conf_file) def _on_cmis_url_changed(self, new_text): # Slot raised when text for CMIS URL changes. Basically updates # the tooltip so as to display long URLs self.txt_atom_pub_url.setToolTip(new_text) def _check_shortcuts_settings(self): """ Checks the state of the show dialog checkbox :return: Bool """ # Check registry values for shortcut dialog show_dlg = 1 dlg_key = self._reg_config.read([SHOW_SHORTCUT_DIALOG]) if len(dlg_key) > 0: show_dlg = dlg_key[SHOW_SHORTCUT_DIALOG] if show_dlg == 1 or show_dlg == unicode(1): self.chk_shortcut_dlg.setChecked(True) elif show_dlg == 0 or show_dlg == unicode(0): self.chk_shortcut_dlg.setChecked(False) def _on_check_shortcut_checkbox(self, toggled): """ Slot raised when the checkbox for showing shortcut window is checked/unchecked. :param toggled: Toggle status :type toggled: bool """ if toggled: self._reg_config.write({SHOW_SHORTCUT_DIALOG: 1}) self.notif_bar.clear() msg = self.tr(u"Shortcuts window will show after login") self.notif_bar.insertWarningNotification(msg) else: self._reg_config.write({SHOW_SHORTCUT_DIALOG: 0}) self.notif_bar.clear() msg = self.tr(u"Shortcuts window will not show after login") self.notif_bar.insertWarningNotification(msg) def load_profiles(self): """ Load existing profiles into the combobox. """ profile_names = self._config.profiles.keys() self.cbo_profiles.clear() self.cbo_profiles.addItem('') self.cbo_profiles.addItems(profile_names) def _load_db_conn_properties(self): # Load database connection properties from the registry. db_conn = self._db_config.read() if not db_conn is None: self.txtHost.setText(db_conn.Host) self.txtPort.setText(db_conn.Port) self.txtDatabase.setText(db_conn.Database) def _load_qgis_pg_connections(self): """ Load QGIS postgres connections. """ self.cbo_pg_connections.addItem('') profiles = pg_profile_names() for profile in profiles: self.cbo_pg_connections.addItem(profile[0], profile[1]) def _load_directory_paths(self): # Load paths to various directory settings. comp_out_path = composer_output_path() comp_temp_path = composer_template_path() if not comp_out_path is None: self.txt_output_dir.setText(comp_out_path) if not comp_temp_path is None: self.txt_template_dir.setText(comp_temp_path) def _on_use_pg_connections(self, state): # Slot raised when to (not) use existing pg connections if not state: self.cbo_pg_connections.setCurrentIndex(0) self.cbo_pg_connections.setEnabled(False) # Restore current connection in registry self._load_db_conn_properties() else: self.cbo_pg_connections.setEnabled(True) def _on_pg_profile_changed(self, index): """ Slot raised when the index of the pg profile changes. If the selection is valid then the system will attempt to extract the database connection properties of the selected profile stored in the registry. """ if index == 0: return profile_path = self.cbo_pg_connections.itemData(index) q_config = QGISRegistryConfig(profile_path) db_items = q_config.read(['Database', 'Host', 'Port']) if len(db_items) > 0: self.txtDatabase.setText(db_items['Database']) self.txtHost.setText(db_items['Host']) self.txtPort.setText(db_items['Port']) def clear_properties(self): """ Clears the host, database name and port number values from the respective controls. """ self.txtDatabase.clear() self.txtHost.clear() self.txtPort.clear() def _on_choose_doc_designer_template_path(self): # Slot raised to select directory for document designer templates. self._set_selected_directory( self.txt_template_dir, self.tr('Document Designer Templates Directory')) def _on_choose_doc_generator_output_path(self): # Slot raised to select directory for doc generator outputs. self._set_selected_directory( self.txt_output_dir, self.tr('Document Generator Output Directory')) def _set_selected_directory(self, txt_box, title): def_path = txt_box.text() sel_doc_path = QFileDialog.getExistingDirectory(self, title, def_path) if sel_doc_path: normalized_path = QDir.fromNativeSeparators(sel_doc_path) txt_box.clear() txt_box.setText(normalized_path) def _validate_db_props(self): # Test if all properties have been specified status = True self.notif_bar.clear() if not self.txtHost.text(): msg = self.tr('Please specify the database host address.') self.notif_bar.insertErrorNotification(msg) status = False if not self.txtPort.text(): msg = self.tr('Please specify the port number.') self.notif_bar.insertErrorNotification(msg) status = False if not self.txtDatabase.text(): msg = self.tr('Please specify the database name.') self.notif_bar.insertErrorNotification(msg) status = False return status def _database_connection(self): # Creates a databaase connection object from the specified args host = self.txtHost.text() port = self.txtPort.text() database = self.txtDatabase.text() # Create database connection object db_conn = DatabaseConnection(host, port, database) return db_conn def _on_test_db_connection(self): """ Slot raised to test database connection. """ status = self._validate_db_props() if not status: return login_dlg = loginDlg(self, True) db_conn = self._database_connection() login_dlg.set_database_connection(db_conn) res = login_dlg.exec_() if res == QDialog.Accepted: msg = self.tr(u"Connection to '{0}' database was " "successful.".format(db_conn.Database)) self.notif_bar.insertSuccessNotification(msg) def _on_test_cmis_connection(self): # Slot raised to test connection to CMIS service status = self._validate_cmis_properties() if not status: return self.notif_bar.clear() atom_pub_url = self.txt_atom_pub_url.text() auth_conf_id = self.cbo_auth_config_name.itemData( self.cbo_auth_config_name.currentIndex()) cmis_mgr = CmisManager(url=atom_pub_url, auth_config_id=auth_conf_id) QgsApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) status = cmis_mgr.connect() QgsApplication.restoreOverrideCursor() if status: msg = self.tr('Connection to the CMIS server succeeded.') self.notif_bar.insertSuccessNotification(msg) else: msg = self.tr( 'Failed to connect to the CMIS server. Check URL and/or ' 'credentials.') self.notif_bar.insertErrorNotification(msg) def set_current_profile(self): """ Saves the given profile name as the current profile. """ profile_name = self.cbo_profiles.currentText() if not profile_name: self.notif_bar.clear() msg = self.tr('Profile name is empty, current profile will not ' 'be set.') self.notif_bar.insertErrorNotification(msg) return False save_current_profile(profile_name) return True def set_cmis_properties(self): """ Saves the CMIS atom pub URL and authentication configuration ID. """ if not self._validate_cmis_properties(): return False atom_pub_url = self.txt_atom_pub_url.text() set_cmis_atom_pub_url(atom_pub_url) curr_idx = self.cbo_auth_config_name.currentIndex() config_id = self.cbo_auth_config_name.itemData(curr_idx) set_cmis_auth_config_id(config_id) return True def _validate_cmis_properties(self): # Assert if user has specified URL and authentication config ID. atom_pub_url = self.txt_atom_pub_url.text() config_name_id = self.cbo_auth_config_name.currentText() status = True if not atom_pub_url: msg = self.tr( 'Please specify the URL of the CMIS atom pub service.') self.notif_bar.insertErrorNotification(msg) status = False if not config_name_id: msg = self.tr( 'Please select the configuration ID containing the CMIS authentication.' ) self.notif_bar.insertErrorNotification(msg) status = False return status def set_holders_config_file(self): # Save the path to the holders configuration file holders_config_file = self.txt_holders_config.text() if not holders_config_file: msg = self.tr( 'Please set the path to the holders configuration file.') self.notif_bar.insertErrorNotification(msg) return False set_holders_config_path(holders_config_file) return True def save_database_properties(self): """ Saves the specified database connection properties to the registry. :return: True if the connection properties were successfully saved. :rtype: bool """ if not self._validate_db_props(): return False # Create a database object and write it to the registry db_conn = self._database_connection() self._db_config.write(db_conn) return True def set_document_templates_path(self): """ Set the directory of document designer templates. :return: True if the directory was set in the registry, otherwise False. :rtype: bool """ path = self.txt_template_dir.text() if not path: msg = self.tr('Please set the document designer templates ' 'directory.') self.notif_bar.insertErrorNotification(msg) return False # Validate path if not self._check_path_exists(path, self.txt_template_dir): return False # Commit to registry self._reg_config.write({COMPOSER_TEMPLATE: path}) return True def set_document_output_path(self): """ Set the directory of document generator outputs. :return: True if the directory was set in the registry, otherwise False. :rtype: bool """ path = self.txt_output_dir.text() if not path: msg = self.tr('Please set the document generator output directory' '.') self.notif_bar.insertErrorNotification(msg) return False # Validate path if not self._check_path_exists(path, self.txt_output_dir): return False # Commit to registry self._reg_config.write({COMPOSER_OUTPUT: path}) return True def _check_path_exists(self, path, text_box): # Validates if the specified folder exists dir = QDir() if not dir.exists(path): msg = self.tr(u"'{0}' directory does not exist.".format(path)) self.notif_bar.insertErrorNotification(msg) # Highlight textbox control text_box.setStyleSheet(INVALIDATESTYLESHEET) timer = QTimer(self) # Sync interval with that of the notification bar timer.setInterval(self.notif_bar.interval) timer.setSingleShot(True) # Remove previous connected slots (if any) receivers = timer.receivers(SIGNAL('timeout()')) if receivers > 0: self._timer.timeout.disconnect() timer.start() timer.timeout.connect(lambda: self._restore_stylesheet(text_box)) return False return True def _restore_stylesheet(self, textbox): # Slot raised to restore the original stylesheet of the textbox control textbox.setStyleSheet(self._default_style_sheet) # Get reference to timer and delete sender = self.sender() if not sender is None: sender.deleteLater() def apply_debug_logging(self): # Save debug logging logger = logging.getLogger('stdm') if self.chk_logging.checkState() == Qt.Checked: logger.setLevel(logging.DEBUG) set_debug_logging(True) else: logger.setLevel(logging.ERROR) set_debug_logging(False) def apply_settings(self): """ Save settings. :return: True if the settings were successfully applied, otherwise False. :rtype: bool """ # Set current profile if not self.set_current_profile(): return False # Set db connection properties if not self.save_database_properties(): return False # Set CMIS properties if not self.set_cmis_properties(): return False # Set holders configuration file path if not self.set_holders_config_file(): return False # Set document designer templates path if not self.set_document_templates_path(): return False # Set document generator output path if not self.set_document_output_path(): return False self.apply_debug_logging() msg = self.tr('Settings successfully saved.') self.notif_bar.insertSuccessNotification(msg) return True def on_accept(self): """ Slot raised to save the settings of the current widget and close the widget. """ if not self.apply_settings(): return self.accept()
class FltsSearchWidget(QWidget, Ui_FltsSearchWidget): """ Widget that provides an interface for searching data from a given data source specified in the search configuration. """ def __init__(self, search_config): """ :param search_config: Search configuration object. :type search_config: FltsSearchConfiguration """ super(FltsSearchWidget, self).__init__(None) self.setupUi(self) self.notif_bar = NotificationBar(self.vlNotification) self._config = search_config self._ds_mgr = FltsSearchConfigDataSourceManager(self._config) # Sort dialog and mapping self._sort_dialog = None self._sort_map = None # Check validity self._check_validity() if not self._ds_mgr.is_valid: return # Initialize UI self._init_gui() def _enable_controls(self, enable): # Enables or disables UI controls self.cbo_column.setEnabled(enable) self.cbo_expression.setEnabled(enable) self.txt_keyword.setEnabled(enable) self.btn_search.setEnabled(enable) self.btn_advanced_search.setEnabled(enable) self.btn_clear.setEnabled(enable) self.tb_results.setEnabled(enable) self.btn_sort.setEnabled(enable) def _check_validity(self): # Notify is the data source is invalid. if not self._ds_mgr.is_valid: self._enable_controls(False) self.notif_bar.insertErrorNotification( u'\'{0}\' data source does not exist in the database.'.format( self._config.data_source)) def _init_gui(self): # Connect signals self.btn_search.clicked.connect(self.on_basic_search) self.cbo_column.currentIndexChanged.connect( self._on_filter_col_changed) self.btn_clear.clicked.connect(self.clear_results) self.btn_advanced_search.clicked.connect(self.on_advanced_search) self.btn_sort.clicked.connect(self.on_sort_columns) self.txt_keyword.returnPressed.connect(self.on_basic_search) # Set filter columns self.cbo_column.clear() col_ico = QIcon(':/plugins/stdm/images/icons/column.png') for col, disp_col in self._ds_mgr.filter_column_mapping.iteritems(): self.cbo_column.addItem(col_ico, disp_col, col) # Set model self._res_model = SearchResultsModel(self._ds_mgr) self.tb_results.setModel(self._res_model) self.tb_results.hideColumn(0) # Connect to item selection changed signal selection_model = self.tb_results.selectionModel() selection_model.selectionChanged.connect(self.on_selection_changed) self.txt_keyword.setFocus() def _on_filter_col_changed(self, idx): # Set the valid expressions based on the type of the filter column. self.cbo_expression.clear() self.txt_keyword.clearValue() if idx == -1: return filter_col = self.cbo_column.itemData(idx) filter_exp = self._ds_mgr.column_type_expression(filter_col) exp_ico = QIcon(':/plugins/stdm/images/icons/math_operators.png') for disp, exp in filter_exp.iteritems(): self.cbo_expression.addItem(exp_ico, disp, exp) # Update the search completer self._set_search_completer() def _set_search_completer(self): # Set the completer for the search line edit for showing # previously saved searches. ds = self._config.data_source filter_col = self.cbo_column.itemData(self.cbo_column.currentIndex()) searches = column_searches(ds, filter_col) # Create and set completer completer = QCompleter(searches, self) completer.setCaseSensitivity(Qt.CaseInsensitive) self.txt_keyword.setCompleter(completer) def clear_results(self): """ Removes any previous search results in the view. """ self._res_model.clear_results() self._update_search_status(-1) def on_basic_search(self): """ Slot raised to execute basic search. """ # Validate if input parameters have been specified filter_col = '' msgs = [] if not self.cbo_column.currentText(): msgs.append('Filter column has not been specified.') else: filter_col = self.cbo_column.itemData( self.cbo_column.currentIndex()) filter_exp = None if not self.cbo_expression.currentText(): msgs.append('Filter expression has not been specified.') else: filter_exp = self.cbo_expression.itemData( self.cbo_expression.currentIndex()) search_term = self.txt_keyword.value() if not search_term: msgs.append('Please specify the search keyword.') # Clear any previous notifications self.notif_bar.clear() # Insert warning messages for msg in msgs: self.notif_bar.insertWarningNotification(msg) if len(msgs) > 0: return # Save search and update completer with historical searches save_column_search(self._config.data_source, filter_col, search_term) self._set_search_completer() # Format the input value depending on the selected operator fm_search_term = self._ds_mgr.format_value_by_operator( filter_exp, search_term) # Build search query object search_query = BasicSearchQuery() search_query.filter_column = filter_col search_query.expression = filter_exp search_query.search_term = fm_search_term search_query.quote_column_value = self._ds_mgr.quote_column_value( filter_col) exp_text = search_query.expression_text() self.exec_search(exp_text) def exec_search(self, search_expression): """ Execute a search operation based on the specified filter expression. :param search_expression: Filter expression. :type search_expression: str """ self.clear_results() if not search_expression: msg = 'Search expression cannot be empty.' self.notif_bar.insertWarningNotification(msg) return try: results = self._ds_mgr.search_data_source(search_expression, self._sort_map) self._update_search_status(len(results)) # Update model self._res_model.set_results(results) # Notify user if there are no results if len(results) == 0: self.notif_bar.insertInformationNotification( 'No results found matching the search keyword.') except FltsSearchException as fe: self.notif_bar.insertWarningNotification(str(fe)) def _update_search_status(self, count=-1): # Updates search count label. txt = '' suffix = 'record' if count == 1 else 'records' if count != -1: # Separate thousand using comma cs_count = format(count, ',') txt = '{0} {1}'.format(cs_count, suffix) self.lbl_results_count.setText(txt) def on_advanced_search(self): # Slot raised to show the expression editor. filter_col = self.cbo_column.itemData(self.cbo_column.currentIndex()) start_txt = '"{0}" = '.format(filter_col) exp_dlg = QgsExpressionBuilderDialog(self._ds_mgr.vector_layer, start_txt, self, self._config.display_name) exp_dlg.setWindowTitle('{0} Expression Editor'.format( self._config.display_name)) if exp_dlg.exec_() == QDialog.Accepted: exp_text = exp_dlg.expressionText() self.exec_search(exp_text) def on_sort_columns(self): # Slot raised to show the sort column dialog col_mapping = self._ds_mgr.valid_column_mapping if not self._sort_dialog: self._sort_dialog = SortColumnDialog(col_mapping, self) if self._sort_dialog.exec_() == QDialog.Accepted: sort_map = self._sort_dialog.sort_mapping() if len(sort_map) > 0: self._sort_map = sort_map else: self._sort_map = None def selected_rows(self): """ :return: Returns the row numbers of the selected results. :rtype: list """ return [ idx.row() for idx in self.tb_results.selectionModel().selectedRows() ] def selected_features(self): """ :return: Returns a list of QgsFeatures corresponding to the selected results. :rtype: list """ features = [] for r in self.selected_rows(): feat = self._res_model.row_to_feature(r) if feat: features.append(feat) return features def on_selection_changed(self, previous_selection, current_selection): # Slot raised when the selection changes in the results table. features = self.selected_features()
class OptionsDialog(QDialog, Ui_DlgOptions): """ Dialog for editing STDM settings. """ def __init__(self, iface): QDialog.__init__(self, iface.mainWindow()) self.setupUi(self) self.iface = iface self.notif_bar = NotificationBar(self.vlNotification, 6000) self._apply_btn = self.buttonBox.button(QDialogButtonBox.Apply) self._reg_config = RegistryConfig() self._db_config = DatabaseConfig() #Connect signals self._apply_btn.clicked.connect(self.apply_settings) self.buttonBox.accepted.connect(self.on_accept) self.chk_pg_connections.toggled.connect(self._on_use_pg_connections) self.cbo_pg_connections.currentIndexChanged.connect( self._on_pg_profile_changed) self.btn_db_conn_clear.clicked.connect(self.clear_properties) self.btn_test_db_connection.clicked.connect(self._on_test_connection) self.btn_supporting_docs.clicked.connect( self._on_choose_supporting_docs_path) self.btn_template_folder.clicked.connect( self._on_choose_doc_designer_template_path) self.btn_composer_out_folder.clicked.connect( self._on_choose_doc_generator_output_path) self.upgradeButton.toggled.connect(self.manage_upgrade) self._config = StdmConfiguration.instance() self._default_style_sheet = self.txtRepoLocation.styleSheet() self.manage_upgrade() self.init_gui() def init_gui(self): #Set integer validator for the port number int_validator = QIntValidator(1024, 49151) self.txtPort.setValidator(int_validator) #Load profiles self.load_profiles() #Set current profile in the combobox curr_profile = current_profile() if not curr_profile is None: setComboCurrentIndexWithText(self.cbo_profiles, curr_profile.name) #Load current database connection properties self._load_db_conn_properties() #Load existing PostgreSQL connections self._load_qgis_pg_connections() #Load directory paths self._load_directory_paths() # Debug logging lvl = debug_logging() if lvl: self.chk_logging.setCheckState(Qt.Checked) else: self.chk_logging.setCheckState(Qt.Unchecked) def load_profiles(self): """ Load existing profiles into the combobox. """ profile_names = self._config.profiles.keys() self.cbo_profiles.clear() self.cbo_profiles.addItem('') self.cbo_profiles.addItems(profile_names) def _load_db_conn_properties(self): #Load database connection properties from the registry. db_conn = self._db_config.read() if not db_conn is None: self.txtHost.setText(db_conn.Host) self.txtPort.setText(db_conn.Port) self.txtDatabase.setText(db_conn.Database) def _load_qgis_pg_connections(self): """ Load QGIS postgres connections. """ self.cbo_pg_connections.addItem('') profiles = pg_profile_names() for profile in profiles: self.cbo_pg_connections.addItem(profile[0], profile[1]) def _load_directory_paths(self): #Load paths to various directory settings. comp_out_path = composer_output_path() comp_temp_path = composer_template_path() source_doc_path = source_documents_path() if not source_doc_path is None: self.txtRepoLocation.setText(source_doc_path) if not comp_out_path is None: self.txt_output_dir.setText(comp_out_path) if not comp_temp_path is None: self.txt_template_dir.setText(comp_temp_path) def _on_use_pg_connections(self, state): #Slot raised when to (not) use existing pg connections if not state: self.cbo_pg_connections.setCurrentIndex(0) self.cbo_pg_connections.setEnabled(False) #Restore current connection in registry self._load_db_conn_properties() else: self.cbo_pg_connections.setEnabled(True) def _on_pg_profile_changed(self, index): """ Slot raised when the index of the pg profile changes. If the selection is valid then the system will attempt to extract the database connection properties of the selected profile stored in the registry. """ if index == 0: return profile_path = self.cbo_pg_connections.itemData(index) q_config = QGISRegistryConfig(profile_path) db_items = q_config.read(['Database', 'Host', 'Port']) if len(db_items) > 0: self.txtDatabase.setText(db_items['Database']) self.txtHost.setText(db_items['Host']) self.txtPort.setText(db_items['Port']) def clear_properties(self): """ Clears the host, database name and port number values from the respective controls. """ self.txtDatabase.clear() self.txtHost.clear() self.txtPort.clear() def _on_choose_supporting_docs_path(self): #Slot raised to select directory for supporting documents. self._set_selected_directory(self.txtRepoLocation, self.tr('Supporting Documents Directory')) def _on_choose_doc_designer_template_path(self): #Slot raised to select directory for document designer templates. self._set_selected_directory( self.txt_template_dir, self.tr('Document Designer Templates Directory')) def _on_choose_doc_generator_output_path(self): #Slot raised to select directory for doc generator outputs. self._set_selected_directory( self.txt_output_dir, self.tr('Document Generator Output Directory')) def _set_selected_directory(self, txt_box, title): def_path = txt_box.text() sel_doc_path = QFileDialog.getExistingDirectory(self, title, def_path) if sel_doc_path: normalized_path = QDir.fromNativeSeparators(sel_doc_path) txt_box.clear() txt_box.setText(normalized_path) def _validate_db_props(self): #Test if all properties have been specified status = True self.notif_bar.clear() if not self.txtHost.text(): msg = self.tr('Please specify the database host address.') self.notif_bar.insertErrorNotification(msg) status = False if not self.txtPort.text(): msg = self.tr('Please specify the port number.') self.notif_bar.insertErrorNotification(msg) status = False if not self.txtDatabase.text(): msg = self.tr('Please specify the database name.') self.notif_bar.insertErrorNotification(msg) status = False return status def _database_connection(self): #Creates a databaase connection object from the specified args host = self.txtHost.text() port = self.txtPort.text() database = self.txtDatabase.text() #Create database connection object db_conn = DatabaseConnection(host, port, database) return db_conn def _on_test_connection(self): """ Slot raised to test database connection. """ status = self._validate_db_props() if not status: return login_dlg = loginDlg(self, True) db_conn = self._database_connection() login_dlg.set_database_connection(db_conn) res = login_dlg.exec_() if res == QDialog.Accepted: msg = self.tr(u"Connection to '{0}' database was " "successful.".format(db_conn.Database)) QMessageBox.information(self, self.tr('Database Connection'), msg) def set_current_profile(self): """ Saves the given profile name as the current profile. """ profile_name = self.cbo_profiles.currentText() if not profile_name: self.notif_bar.clear() msg = self.tr('Profile name is empty, current profile will not ' 'be set.') self.notif_bar.insertErrorNotification(msg) return False save_current_profile(profile_name) return True def save_database_properties(self): """ Saves the specified database connection properties to the registry. :return: True if the connection properties were successfully saved. :rtype: bool """ if not self._validate_db_props(): return False #Create a database object and write it to the registry db_conn = self._database_connection() self._db_config.write(db_conn) return True def set_supporting_documents_path(self): """ Set the directory of supporting documents. :return: True if the directory was set in the registry, otherwise False. :rtype: bool """ path = self.txtRepoLocation.text() if not path: msg = self.tr('Please set the supporting documents directory.') self.notif_bar.insertErrorNotification(msg) return False #Validate path if not self._check_path_exists(path, self.txtRepoLocation): return False #Commit to registry self._reg_config.write({NETWORK_DOC_RESOURCE: path}) return True def set_document_templates_path(self): """ Set the directory of document designer templates. :return: True if the directory was set in the registry, otherwise False. :rtype: bool """ path = self.txt_template_dir.text() if not path: msg = self.tr('Please set the document designer templates ' 'directory.') self.notif_bar.insertErrorNotification(msg) return False #Validate path if not self._check_path_exists(path, self.txt_template_dir): return False #Commit to registry self._reg_config.write({COMPOSER_TEMPLATE: path}) return True def set_document_output_path(self): """ Set the directory of document generator outputs. :return: True if the directory was set in the registry, otherwise False. :rtype: bool """ path = self.txt_output_dir.text() if not path: msg = self.tr('Please set the document generator output directory' '.') self.notif_bar.insertErrorNotification(msg) return False #Validate path if not self._check_path_exists(path, self.txt_output_dir): return False #Commit to registry self._reg_config.write({COMPOSER_OUTPUT: path}) return True def _check_path_exists(self, path, text_box): #Validates if the specified folder exists dir = QDir() if not dir.exists(path): msg = self.tr(u"'{0}' directory does not exist.".format(path)) self.notif_bar.insertErrorNotification(msg) #Highlight textbox control text_box.setStyleSheet(INVALIDATESTYLESHEET) timer = QTimer(self) #Sync interval with that of the notification bar timer.setInterval(self.notif_bar.interval) timer.setSingleShot(True) #Remove previous connected slots (if any) receivers = timer.receivers(SIGNAL('timeout()')) if receivers > 0: self._timer.timeout.disconnect() timer.start() timer.timeout.connect(lambda: self._restore_stylesheet(text_box)) return False return True def _restore_stylesheet(self, textbox): # Slot raised to restore the original stylesheet of the textbox control textbox.setStyleSheet(self._default_style_sheet) # Get reference to timer and delete sender = self.sender() if not sender is None: sender.deleteLater() def apply_debug_logging(self): # Save debug logging logger = logging.getLogger('stdm') if self.chk_logging.checkState() == Qt.Checked: logger.setLevel(logging.DEBUG) set_debug_logging(True) else: logger.setLevel(logging.ERROR) set_debug_logging(False) def apply_settings(self): """ Save settings. :return: True if the settings were successfully applied, otherwise False. :rtype: bool """ #Set current profile if not self.set_current_profile(): return False #Set db connection properties if not self.save_database_properties(): return False #Set supporting documents directory if not self.set_supporting_documents_path(): return False #Set document designer templates path if not self.set_document_templates_path(): return False #Set document generator output path if not self.set_document_output_path(): return False self.apply_debug_logging() msg = self.tr('Settings successfully saved.') self.notif_bar.insertSuccessNotification(msg) return True def on_accept(self): """ Slot raised to save the settings of the current widget and close the widget. """ if not self.apply_settings(): return self.accept() def manage_upgrade(self): """ A slot raised when the upgrade button is clicked. It disables or enables the upgrade button based on the ConfigUpdated registry value. """ self.config_updated_dic = self._reg_config.read([CONFIG_UPDATED]) # if config file exists, check if registry key exists if len(self.config_updated_dic) > 0: config_updated_val = self.config_updated_dic[CONFIG_UPDATED] # If failed to upgrade, enable the upgrade button if config_updated_val == '0' or config_updated_val == '-1': self.upgradeButton.setEnabled(True) # disable the button if any other value. else: self.upgradeButton.setEnabled(False) else: self.upgradeButton.setEnabled(False)
class ProfileEditor(QDialog, Ui_Profile): def __init__(self, parent): QDialog.__init__(self, parent) self.profile_name = '' self.desc = '' self.setupUi(self) self.init_controls() self.notice_bar = NotificationBar(self.notif_bar) def init_controls(self): self.edtProfile.clear() self.edtDesc.clear() self.edtProfile.setFocus() self.edtProfile.textChanged.connect(self.validate_text) def format_name(self, txt): ''''remove any trailing spaces in the name and replace them underscore''' formatted_name = txt.strip().replace(' ', "_") return formatted_name def add_profile(self): self.profile_name = self.format_name(unicode(self.edtProfile.text())) self.desc = unicode(self.edtDesc.text()) def show_notification(self, message): """ Shows a warning notification bar message. :param message: The message of the notification. :type message: String """ msg = self.trUtf8(message) self.notice_bar.clear() self.notice_bar.insertErrorNotification(msg) def validate_text(self, text): """ Validates and updates the entered text if necessary. Spaces are replaced by _ and capital letters are replaced by small. :param text: The text entered :type text: String """ text_edit = self.sender() cursor_position = text_edit.cursorPosition() text_edit.setValidator(None) if len(text) == 0: return locale = QSettings().value("locale/userLocale")[0:2] if locale == 'en': name_regex = QRegExp('^(?=.{0,40}$)[ _a-zA-Z][a-zA-Z0-9_ ]*$') name_validator = QRegExpValidator(name_regex) text_edit.setValidator(name_validator) QApplication.processEvents() last_character = text[-1:] state = name_validator.validate(text, text.index(last_character))[0] if state != QValidator.Acceptable: msg = u'\'{0}\' is not allowed at this position.'.format( last_character ) self.show_notification(msg) text = text[:-1] # remove space and underscore at the beginning of the text if len(text) > 1: if text[0] == ' ' or text[0] == '_': text = text[1:] self.blockSignals(True) text_edit.setText(text) text_edit.setCursorPosition(cursor_position) self.blockSignals(False) text_edit.setValidator(None) def accept(self): '''listen to user action on the dialog''' if self.edtProfile.text() == '' or self.edtProfile.text() == ' ': self.error_info_message( QApplication.translate( "ProfileEditor", "Please enter a valid Profile name." ) ) return self.add_profile() self.done(1) def reject(self): self.done(0) def error_info_message(self, Message): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("STDM") msg.setText(Message) msg.exec_()
class ProfileInstanceRecords(QDialog, FORM_CLASS): """ class constructor The class handles all the instances that the user has collected and saved in the folder and saved in a computer. The class will construct the path to the folder and enumerate all available instances and return the count. It will also rename all the file based on instance unique GUUID for easier management and future updates. """ def __init__(self, parent=None): """ initailize class variables here """ super(ProfileInstanceRecords, self).__init__(parent) self.setupUi(self) self.path = None self.instance_list = [] self.relations = OrderedDict() self.parent_ids = {} self.importlogger = ImportLogger() self._notif_bar_str = NotificationBar(self.vlnotification) self.chk_all.setCheckState(Qt.Checked) self.entity_model = EntitiesModel() self.uuid_extractor = InstanceUUIDExtractor(self.path) self.btn_chang_dir.setIcon(QIcon(":/plugins/stdm/images/icons/open_file.png")) self.btn_refresh.setIcon(QIcon(":/plugins/stdm/images/icons/update.png")) self.chk_all.stateChanged.connect(self.change_check_state) #self.cbo_profile.currentIndexChanged.connect(self.current_profile_changed) self.btn_chang_dir.clicked.connect(self.on_directory_search) self.lst_widget.itemClicked.connect(self.user_selected_entities) self.btn_refresh.clicked.connect(self.update_files_with_custom_filter) self.buttonBox.button(QDialogButtonBox.Save).setText('Import') #self.load_config() self.init_file_path() self.current_profile_changed() self.change_check_state(self.chk_all.checkState()) self.instance_dir() def load_config(self): """ Load STDM configuration :return: """ stdm_config = None if QFile.exists(HOME+"/stdm/configuration.stc"): stdm_config = QFile(CONFIG_FILE) ConfigurationFileSerializer(stdm_config) profiles = StdmConfiguration.instance().profiles return profiles def change_check_state(self, state): """ Change the check state of items in a list widget """ for i in range(self.lst_widget.count()): self.lst_widget.item(i).setCheckState(state) def profiles(self): """ Return a list of all profiles :rtype: list """ return self.load_config().values() def current_profile_changed(self): """ Get the current profile so that it is the one selected at the combo box :return: """ self.instance_list = [] self.active_profile() self.init_file_path() self.available_records() self.on_dir_path() self.populate_entities_widget() def active_profile(self): """ get the user selected profile :return:p """ self.profile = current_profile().name return self.profile def instance_dir(self): """ Create a path where imported instance will be kept :return: """ self.inst_path = CONFIG_FILE+"_imported" if not os.access(self.inst_path, os.F_OK): os.makedirs(unicode(self.inst_path)) else: return self.inst_path def imported_instance_path(self): """ :return: """ self.instance_dir() return self.inst_path def init_file_path(self): """ Initialize GeoODK file path """ self.path = self.geoODK_file_path(self.txt_directory.text()) self.txt_directory.setText(self.path) def geoODK_file_path(self, path=''): """ Check if geoODK file path has been configured, if not configure default and return it. :rtype: string """ if not path.strip(): path = self.make_path(GEOODK_FORM_HOME) return path def make_path(self, path): """ Create and return a file path if is not available. :rtype: string """ if not os.access(path, os.F_OK): os.makedirs(unicode(path)) return path def xform_xpaths(self): """ Return the full path to the default config path and filter geoodk instance that matches the current profile path :return: directories :rtype: list """ return [os.path.join(self.path, name) for name in os.listdir(self.path) if os.path.isdir(os.path.join(self.path, name)) if name.startswith(self.profile_formater())] def on_dir_path(self): """ Extract the specific folder information and rename the file :return: """ self.uuid_extractor.new_list = [] if self.record_count() > 0: directories = self.xform_xpaths() for directory in directories: self.extract_guuid_and_rename_file(directory) inst_count = len(self.instance_list) rm_count = self.remove_imported_instances() diff = inst_count - rm_count self.txt_count.setText(unicode(diff)) def extract_guuid_and_rename_file(self, path): """ Extract the unique Guuid and rename the file so that we can uniquely identify each file :return: """ for f in os.listdir(path): if os.path.isfile(os.path.join(path, f)) and f.endswith('.xml'): file_instance = os.path.join(path, f) self.rename_file_to_UUID(file_instance) def read_instance_data(self): """Read all instance data once and store them in a dict :rtype: dict """ mobile_data = OrderedDict() social_tenure_info = OrderedDict() for instance in self.instance_list: self.uuid_extractor.set_file_path(instance) field_data_nodes = self.uuid_extractor.document_entities_with_data(current_profile().name.replace(' ', '_'), self.user_selected_entities()) str_data_nodes = self.uuid_extractor.document_entities_with_data(current_profile().name.replace(' ', '_'), ['social_tenure']) mobile_data[instance] = [field_data_nodes, str_data_nodes] self.uuid_extractor.close_document() return mobile_data def rename_file_to_UUID(self, file): """ Extract the UUID from each folder and file :return: """ self.uuid_extractor.set_file_path(file) self.uuid_extractor.on_file_passed() self.instance_list = self.uuid_extractor.new_list def move_imported_file(self, file): """ Moves the imported files to avoid repetition :return: """ instance_path = self.imported_instance_path() try: basename = os.path.basename(os.path.dirname(file)) if not os.path.isdir(os.path.join(self.imported_instance_path(), basename)): shutil.move(os.path.dirname(file), instance_path) except Exception as ex: return ex def populate_entities_widget(self): """ Add entities in the instance file into a list view widget """ self.lst_widget.clear() entities = self.instance_entities() if len(entities) > 0: for entity in entities: list_widget = QListWidgetItem( current_profile().entity_by_name(entity).short_name, self.lst_widget) list_widget.setCheckState(Qt.Checked) def user_selected_entities(self): """ :rtype: list """ entities= [] count = self.lst_widget.count() if count > 0: for i in range(count): item = self.lst_widget.item(i) if item.checkState() == Qt.Checked: entities.append(current_profile().entity(item.text()).name) return entities def instance_entities(self): """ Enumerate the entities that are in the current profile and also that are captured in the form so that we are only importing relevant entities to database :return: entities """ current_entities = [] entity_collections = [] instance_collections = self.instance_collection() if len(instance_collections) > 0: for entity_name in self.profile_entities_names(current_profile()): if current_profile().entity_by_name(entity_name) is not None: current_entities.append(entity_name) return current_entities def instance_collection(self): """ Enumerate all the instances found in the instance directory rtype: list """ dirs = self.xform_xpaths() instance_collections = [] if len(dirs) > 0: for dir_f in dirs: xml_files = [dir_f.replace("\\", "/")+'/'+f for f in os.listdir(dir_f) if f.endswith('.xml')] if len(xml_files)>0: instance_collections.append(xml_files[0]) return instance_collections def check_profile_with_custom_name(self): """ Try extract mobile instance with custom filter name. Assumption is that there is a profile that bears that name :return: """ mismatch_profile = 'Nothing found to import. \n' \ ' Ensure the current filter text or profile is correct' entity_attr = [] if self.txt_filter.text()!= '': for obj in self.profiles(): if obj.name.startswith(self.txt_filter.text()): if obj.name != current_profile().name: self._notif_bar_str.insertErrorNotification(mismatch_profile) return return self.uuid_extractor.document_entities(self.profile) def profile_entities_names(self, profile): """ Return names of all entities in a profile :rtype: list """ entities_names = [] for entity in profile.user_entities(): entities_names.append(entity.name) return entities_names def has_foreign_keys_parent(self, select_entities): """ Ensure we check that the table is not parent else import parent table first Revised in version 1.7. It explicitly assumes str is captured. before it was optional. :return: """ has_relations = False str_tables = current_profile().social_tenure party_tbls = str_tables.parties sp_tbls = str_tables.spatial_units self.relations = OrderedDict() if len(self.instance_list) > 0: if self.uuid_extractor.has_str_captured_in_instance(self.instance_list[0]): for party_tbl in party_tbls: self.relations[party_tbl.name] = ['social_tenure_relationship', party_tbl.short_name.lower() + '_id'] for sp_tbl in sp_tbls: self.relations[sp_tbl.name] = ['social_tenure_relationship', sp_tbl.short_name.lower() + '_id'] # print self.relations for table in select_entities: table_object = current_profile().entity_by_name(table) cols = table_object.columns.values() for col in cols: if col.TYPE_INFO == 'FOREIGN_KEY': parent_object = table_object.columns[col.name] if parent_object.parent: if parent_object.parent.name in self.relations: self.relations[parent_object.parent.name].append([table, col.name]) else: self.relations[parent_object.parent.name] = [table, col.name] #self.relations[parent_object.parent.name].append([table, col.name]) has_relations = True else: continue return has_relations def parent_table_isselected(self): """ Take note that the user selected tables may or may not be imported based on parent child table relationship. Add those table silently so that we can show them to the user :return: """ try: silent_list = [] entities = self.user_selected_entities() if len(entities) > 0: for table in self.relations.keys(): if table not in entities: silent_list.append(table) return silent_list except Exception as ex: self._notif_bar_str.insertErrorNotification(ex.message) def archive_this_import_file(self, counter, instance): """ Ensure that only import are done once :return: """ try: self.importlogger.logger_sections() file_info = 'File instance ' + str(counter)+ ' : \n' + instance self.importlogger.log_action(file_info) except IOError as io: self._notif_bar_str.insertErrorNotification(MSG + ": "+io.message) pass def log_table_entry(self, instance): """ Ensure that only import are done once :return: """ try: current_time = QDateTime() import_time = current_time.currentDateTime() log_entry = instance + ' '+ str(import_time.toPyDateTime()) self.importlogger.log_action(log_entry) except IOError as io: self._notif_bar_str.insertErrorNotification(MSG + ": "+io.message) pass def check_previous_import(self): """ Ensure we are importing files once :return: """ try: self.importlogger.add_log_info() for files in self.instance_list: current_dir = os.path.basename(files) exist = self.importlogger.check_file_exist(current_dir) if exist: self.instance_list.remove(files) self.txt_count.setText(str(len(self.instance_list))) if self.record_count() != len(self.instance_list): msg = 'Some files have been already imported and therefore ' \ 'not enumerated' self._notif_bar_str.insertErrorNotification(msg) except IOError as io: self._notif_bar_str.insertErrorNotification(MSG + ": "+io.message) pass def available_records(self): """ Let the user know how many records have been collected and are available for inport process :return: """ self.txt_count.setText(unicode(self.record_count())) def record_count(self): """ get the count of instance dir in the selected directory :return: integer """ return len([name for name in os.listdir(self.path) if os.path.isdir(os.path.join(self.path, name)) if name.startswith(self.profile_formater())]) def profile_formater(self): """ Format the profile name by removing underscore character :return: """ if self.txt_filter.text() != '': filter_text = self.txt_filter.text() return filter_text else: return self.profile def update_files_with_custom_filter(self): """ Get the new file count with the user custom filter text :return: file count """ self.available_records() self.on_dir_path() self.populate_entities_widget() def projection_settings(self): """ let user select the projections for the data :return: """ project_select = ProjectionSelector(self) projection = project_select.loadAvailableSystems() self.txt_srid.setText(str(projection)) def on_projection_select(self): """ Get the selected projection and set it during data import :return: """ vals = self.txt_srid.text().split(":") return vals[1] def on_directory_search(self): """ Let the user choose the directory with instances :return: """ home_path = 'home' if self.txt_directory.text() != '': home_path = self.txt_directory.text() dir_name = QFileDialog.getExistingDirectory( self, 'Open Directory', home_path, QFileDialog.ShowDirsOnly ) if dir_name: self.txt_directory.setText(str(dir_name)) self.current_profile_changed() self.change_check_state(self.chk_all.checkState()) def feedback_message(self, msg): """ Create a dialog box to capture and display errrors related to db while importing data :param: msg :type: string :return:Qdialog """ msgbox = QMessageBox() msgbox.setStandardButtons(QMessageBox.Ok | QMessageBox.No) msgbox.setWindowTitle("Data Import") msgbox.setText(msg) msgbox.exec_() msgbox.show() return msgbox def save_instance_data_to_db(self, entities): """ Get the user selected entities and insert them into database params: selected entities rtype: list :return:Object :type: dbObject """ cu_obj = '' import_status = False self.txt_feedback.clear() self.txt_feedback.append("Import started, please wait...\n") QCoreApplication.processEvents() self._notif_bar_str.clear() mobile_field_data = self.read_instance_data() self.has_foreign_keys_parent(entities) #print self.relations if len(self.parent_table_isselected()) > 0: if QMessageBox.information(self, QApplication.translate('GeoODKMobileSettings', " Import Warning"), QApplication.translate('GeoODKMobileSettings', 'Some of dependent tables (entities)' 'which may not be part of the selected tables ' 'I.e: {} will be imported' .format(self.parent_table_isselected())), QMessageBox.Ok | QMessageBox.No) == QMessageBox.No: return try: counter = 0 if len(mobile_field_data) > 0: self.pgbar.setRange(counter, len(self.instance_list)) self.pgbar.setValue(0) self.importlogger.log_action("Import started ...\n") for instance_obj, instance_obj_data in mobile_field_data.iteritems(): self.importlogger.log_action("File {} ...\n".format(instance_obj)) parents_info = [] import_status = False counter = counter + 1 self.parent_ids = {} single_occuring, repeated_entities = self.uuid_extractor.attribute_data_from_nodelist( instance_obj_data[0]) for entity, entity_data in single_occuring.iteritems(): import_status = False if entity in self.relations.keys(): if entity in self.parent_ids: continue self.count_import_file_step(counter, entity) log_timestamp = '=== parent table import === : {0}'.format(entity) cu_obj = entity self.log_table_entry(log_timestamp) entity_add = Save2DB(entity, entity_data) entity_add.objects_from_supporting_doc(instance_obj) ref_id = entity_add.save_parent_to_db() import_status = True self.parent_ids[entity] = [ref_id, entity] #log_timestamp = ' --- import succeeded: {0}' .format(str(import_status)) #self.log_table_entry(log_timestamp) parents_info.append(entity) single_occuring.pop(entity) elif entity not in self.relations.keys(): import_status = False for fk_table_name in self.relations.keys(): if fk_table_name not in self.parent_ids: in_relations = [_item for subitem in self.relations[fk_table_name] for _item in subitem] if entity in in_relations: fk_table_data = single_occuring[fk_table_name] entity_add = Save2DB(fk_table_name, fk_table_data) ref_id = entity_add.save_parent_to_db() self.parent_ids[fk_table_name] = [ref_id, fk_table_name] continue self.count_import_file_step(counter, entity) log_timestamp = '=== standalone table import === : {0}'.format(entity) cu_obj = entity self.log_table_entry(log_timestamp) entity_add = Save2DB(entity, entity_data, self.parent_ids) entity_add.objects_from_supporting_doc(instance_obj) child_id = entity_add.save_to_db() cu_obj = entity import_status = True parents_info.append(entity) if entity in self.parent_ids: continue else: self.parent_ids[entity] = [child_id, entity] entity_add.cleanup() if repeated_entities: #self.log_table_entry(" ========== starting import of repeated tables ============") import_status = False for repeated_entity, entity_data in repeated_entities.iteritems(): """We are assuming that the number of repeat table cannot exceed 99""" enum_index = repeated_entity[:2] if enum_index.isdigit(): repeat_table = repeated_entity[2:] else: enum_index = repeated_entity[:1] repeat_table = repeated_entity[1:] log_timestamp = ' child table {0} >> : {1}' \ .format(repeat_table, enum_index) self.count_import_file_step(counter, repeat_table) self.importlogger.log_action(log_timestamp) if repeat_table in self.profile_entities_names(current_profile()): entity_add = Save2DB(repeat_table, entity_data, self.parent_ids) entity_add.objects_from_supporting_doc(instance_obj) child_id = entity_add.save_to_db() cu_obj = repeat_table import_status = True self.log_table_entry(" ------ import succeeded: {0} ".format(import_status)) entity_add.cleanup() else: continue if instance_obj_data[1]: '''We treat social tenure entities separately because of foreign key references''' entity_relation = EntityImporter(instance_obj) single_str, multiple_str = self.uuid_extractor.attribute_data_from_nodelist( instance_obj_data[1]) self.txt_feedback.append('----Creating social tenure relationship') if len(single_str)>0: entity_relation.process_social_tenure(single_str, self.parent_ids) elif len(multiple_str)>1: for repeated_entity, entity_data in multiple_str.iteritems(): """We are assuming that the number of repeat str cannot exceed 10""" entity_relation.process_social_tenure(entity_data, self.parent_ids) self.log_table_entry(" ----- saving social tenure relationship") entity_add.cleanup() self.txt_feedback.append('saving record "{0}" to database'.format(counter)) self.pgbar.setValue(counter) QCoreApplication.processEvents() self.log_instance(instance_obj) self.txt_feedback.append('Number of records successfully imported: {}' .format(counter)) else: self._notif_bar_str.insertErrorNotification("No available records to import") self.pgbar.setValue(0) return except SQLAlchemyError as ae: QCoreApplication.processEvents() QApplication.restoreOverrideCursor() self.feedback_message(unicode(ae.message)) self.txt_feedback.append("current table {0}import failed...\n".format(cu_obj)) self.txt_feedback.append(str(ae.message)) self.log_table_entry(unicode(ae.message)) return def count_import_file_step(self, count = None, table = None): """ Tracking method to record the current import activity :param count: int :param table: string :return: """ self.txt_feedback.append(' Table : {}'.format(table)) def accept(self): """ Execute the import dialog once the save button has been clicked :return: """ self.buttonBox.setEnabled(False) QApplication.setOverrideCursor(Qt.WaitCursor) try: if self.lst_widget.count() < 1: msg = 'No mobile records found for the current profile' self._notif_bar_str.insertErrorNotification(msg) self.buttonBox.setEnabled(True) QApplication.restoreOverrideCursor() return entities = self.user_selected_entities() if len(entities) < 1: if QMessageBox.information(self, QApplication.translate('MobileForms', 'Import Warning'), QApplication.translate('MobileForms', 'You have not ' 'selected any entity for import. All entities ' 'will be imported'), QMessageBox.Ok | QMessageBox.No) == QMessageBox.Ok: entities = self.instance_entities() else: self.buttonBox.setEnabled(True) return self.save_instance_data_to_db(entities) self.buttonBox.setEnabled(True) self.buttonBox.button(QDialogButtonBox.Save).setEnabled(False) QApplication.restoreOverrideCursor() except Exception as ex: self.feedback_message(ex.message) self.log_table_entry(unicode(ex.message)) self.buttonBox.setEnabled(True) QApplication.restoreOverrideCursor() return def log_instance(self, instance): instance_short_name = self.importlogger.log_data_name(instance) log_data = self.importlogger.read_log_data() log_data[instance_short_name] = self.importlogger.log_date() self.importlogger.write_log_data(log_data) def remove_imported_instances(self): count = 0 del_list = [] log_data = self.importlogger.read_log_data() if len(log_data) > 0: for instance in self.instance_list: instance_short_name = self.importlogger.log_data_name(instance) if instance_short_name in log_data: del_list.append(instance) count += 1 for inst in del_list: self.instance_list.remove(inst) return count
class SourceDocumentTranslatorDialog(QDialog, Ui_SourceDocumentTranslatorDialog, TranslatorDialogBase): """ Dialog for defining configuration settings for the SourceDocumentTranslator implementation. """ def __init__(self, parent, source_cols, dest_table, dest_col, src_col): QDialog.__init__(self, parent) self.setupUi(self) TranslatorDialogBase.__init__( self, source_cols, dest_table, dest_col, src_col ) self._notif_bar = NotificationBar(self.vlNotification) #Assert if the entity supports documents self._assert_entity_supports_documents() #Set the source document directory self.source_document_directory = None #Document type name self._document_type_name = self._dest_col #Document type ID self._document_type_id = None #Set document type ID self._set_document_type_id() #Connect slots self.btn_source_doc_folder.clicked.connect( self._load_source_document_directory_selector ) @property def document_type_id(self): """ :return: Returns source document type id. Otherwise None if not set. :rtype: int """ return self._document_type_id @property def document_type(self): """ :return: Returns the name of the selected document type. :rtype: str """ return self._document_type_name @property def documents_directory(self): """ :return: Returns the specified root directory for supporting documents. Otherwise None if not specified by the user. :rtype: str """ return self.txtRootFolder.text() def _assert_entity_supports_documents(self): #Check if the entity supports documents and close automatically if not. entity = self.entity() if entity is None: msg = self.tr('Invalid table object') raise RuntimeError(msg) if not entity.supports_documents: msg = self.tr('The selected destination table does not support ' 'documents.\nHence, this translator is not ' 'applicable.') raise RuntimeError(msg) def _set_document_type_id(self): #Load document id based on the name entity = self.entity() if entity is None: return vl_cls = entity_model( entity.supporting_doc.document_type_entity, entity_only=True ) vl_obj = vl_cls() res = vl_obj.queryObject().all() for r in res: if r.value == self._dest_col: self._document_type_id = r.id break if not entity.supports_documents: msg = self.tr('The selected column does not correspond to a ' 'document type.\nHence, this translator is not ' 'applicable.') title = self.tr('Invalid Document Type') QMessageBox.critical(self, title, msg) #Close dialog self.reject() def _load_source_document_directory_selector(self): #Load file dialog for selecting source documents directory title = self.tr('Select Source Document Directory') def_path = self.txtRootFolder.text() #Use the last set source document directory if not def_path: def_path = last_document_path() sel_doc_path = QFileDialog.getExistingDirectory(self, title, def_path) if sel_doc_path: normalized_path = QDir.fromNativeSeparators(sel_doc_path) self.txtRootFolder.clear() self.txtRootFolder.setText(normalized_path) def value_translator(self): source_doc_translator = SourceDocumentTranslator() source_doc_translator.set_referencing_table(self._dest_table) source_doc_translator.set_referencing_column(self._dest_col) #Just use the source column for getting the relative image path # and name source_doc_translator.add_source_reference_column( self._src_col, self._dest_col ) source_doc_translator.entity = self.entity() source_doc_translator.document_type_id = self._document_type_id source_doc_translator.document_type = self._document_type_name source_doc_translator.source_directory = self.documents_directory return source_doc_translator def validate(self): """ :return: Return True if the source document directory exists, otherwise False. :rtype: bool """ source_doc_path = self.txtRootFolder.text() #Clear previous notifications self._notif_bar.clear() if not source_doc_path: msg = self.tr( 'Please set the root directory of source documents.' ) self._notif_bar.insertErrorNotification(msg) return False dir = QDir() if not dir.exists(source_doc_path): msg = self.tr(u"'{0}' directory does not exist.".format( source_doc_path)) self._notif_bar.insertErrorNotification(msg) return False return True def accept(self): """ Validate before accepting user input. """ if self.validate(): super(SourceDocumentTranslatorDialog, self).accept()
class ViewSTRWidget(WIDGET, BASE): """ Search and browse the social tenure relationship of all participating entities. """ def __init__(self, plugin): QMainWindow.__init__(self, plugin.iface.mainWindow()) self.setupUi(self) self.btnSearch.setIcon(GuiUtils.get_icon('search.png')) self.btnClearSearch.setIcon(GuiUtils.get_icon('reset.png')) self._plugin = plugin self.search_done = False # self.tbPropertyPreview.set_iface(self._plugin.iface) QTimer.singleShot( 100, lambda: self.tbPropertyPreview.set_iface(self._plugin.iface)) self.curr_profile = current_profile() self.spatial_units = self.curr_profile.social_tenure.spatial_units # Center me self.move(QDesktopWidget().availableGeometry().center() - self.frameGeometry().center()) self.sp_unit_manager = SpatialUnitManagerDockWidget( self._plugin.iface, self._plugin) self.geom_cols = [] for spatial_unit in self.spatial_units: each_geom_col = self.sp_unit_manager.geom_columns(spatial_unit) self.geom_cols.extend(each_geom_col) # Configure notification bar self._notif_search_config = NotificationBar(self.vl_notification) # set whether currently logged in user has # permissions to edit existing STR records self._can_edit = self._plugin.STRCntGroup.canUpdate() self._can_delete = self._plugin.STRCntGroup.canDelete() self._can_create = self._plugin.STRCntGroup.canCreate() # Variable used to store a reference to the # currently selected social tenure relationship # when displaying documents in the supporting documents tab window. # This ensures that there are no duplicates # when the same item is selected over and over again. self._strID = None self.removed_docs = None # Used to store the root hash of the currently selected node. self._curr_rootnode_hash = "" self.str_model, self.str_doc_model = entity_model( self.curr_profile.social_tenure, False, True) self._source_doc_manager = SourceDocumentManager( self.curr_profile.social_tenure.supporting_doc, self.str_doc_model, self) self._source_doc_manager.documentRemoved.connect( self.onSourceDocumentRemoved) self._source_doc_manager.setEditPermissions(False) self.initGui() self.add_spatial_unit_layer() self.details_tree_view = DetailsTreeView(iface, self._plugin, self) # else: # self.details_tree_view = self._plugin.details_tree_view self.details_tree_view.activate_feature_details(True) self.details_tree_view.add_tree_view() self.details_tree_view.model.clear() count = pg_table_count(self.curr_profile.social_tenure.name) self.setWindowTitle( self.tr('{}{}'.format(self.windowTitle(), '- ' + str(count) + ' rows'))) self.active_spu_id = -1 self.toolBox.setStyleSheet(''' QToolBox::tab { background: qlineargradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #EDEDED, stop: 0.4 #EDEDED, stop: 0.5 #EDEDED, stop: 1.0 #D3D3D3 ); border-radius: 2px; border-style: outset; border-width: 2px; height: 100px; border-color: #C3C3C3; } QToolBox::tab:selected { font: italic; } ''') self.details_tree_view.view.setStyleSheet(''' QTreeView:!active { selection-background-color: #72a6d9; } ''') def add_tool_buttons(self): """ Add toolbar buttons of add, edit and delete buttons. :return: None :rtype: NoneType """ tool_buttons = QToolBar() tool_buttons.setObjectName('form_toolbar') tool_buttons.setIconSize(QSize(16, 16)) self.addSTR = QAction(GuiUtils.get_icon('add.png'), QApplication.translate('ViewSTRWidget', 'Add'), self) self.editSTR = QAction(GuiUtils.get_icon('edit.png'), QApplication.translate('ViewSTRWidget', 'Edit'), self) self.deleteSTR = QAction( GuiUtils.get_icon('remove.png'), QApplication.translate('ViewSTRWidget', 'Remove'), self) tool_buttons.addAction(self.addSTR) tool_buttons.addAction(self.editSTR) tool_buttons.addAction(self.deleteSTR) self.toolbarVBox.addWidget(tool_buttons) def initGui(self): """ Initialize widget """ self.tb_actions.setVisible(False) self._load_entity_configurations() self.add_tool_buttons() # Connect signals self.tbSTREntity.currentChanged.connect(self.entityTabIndexChanged) self.btnSearch.clicked.connect(self.searchEntityRelations) self.btnClearSearch.clicked.connect(self.clearSearch) # self.tvSTRResults.expanded.connect(self.onTreeViewItemExpanded) # Set the results treeview to accept requests for context menus # self.tvSTRResults.setContextMenuPolicy(Qt.CustomContextMenu) # self.tvSTRResults.customContextMenuRequested.connect( # self.onResultsContextMenuRequested # ) if not self._can_create: self.addSTR.hide() if not self._can_edit: self.editSTR.hide() else: self.editSTR.setDisabled(True) if not self._can_delete: self.deleteSTR.hide() else: self.deleteSTR.setDisabled(True) self.addSTR.triggered.connect(self.load_new_str_editor) self.deleteSTR.triggered.connect(self.delete_str) self.editSTR.triggered.connect(self.load_edit_str_editor) # Load async for the current widget self.entityTabIndexChanged(0) def init_progress_dialog(self): """ Initializes the progress dialog. """ self.progress = QProgressBar(self) self.progress.resize(self.width(), 10) self.progress.setTextVisible(False) def add_spatial_unit_layer(self): """ Add the spatial unit layer into the map canvas for later use. """ # Used for startup of view STR, just add the first geom layer. if len(self.geom_cols) > 0: for spatial_unit in self.spatial_units: layer_name_item = self.sp_unit_manager.geom_col_layer_name( spatial_unit.name, self.geom_cols[0]) self.sp_unit_manager.add_layer_by_name(layer_name_item) def _check_permissions(self): """ Enable/disable actions based on the permissions defined in the content group. """ if self._can_edit: self.tb_actions.addAction(self._new_str_action) else: self.tb_actions.removeAction(self._new_str_action) if len(self.tb_actions.actions()) == 0: self.tb_actions.setVisible(False) else: self.tb_actions.setVisible(True) def _load_entity_configurations(self): """ Specify the entity configurations. """ try: self.parties = self.curr_profile.social_tenure.parties tb_str_entities = self.parties + self.spatial_units for i, t in enumerate(tb_str_entities): QApplication.processEvents() entity_cfg = self._entity_config_from_profile( str(t.name), t.short_name) if not entity_cfg is None: entity_widget = self.add_entity_config(entity_cfg) # entity_widget.setNodeFormatter( # EntityNodeFormatter( # entity_cfg, self.tvSTRResults, self # ) # ) except DummyException as pe: self._notif_search_config.clear() self._notif_search_config.insertErrorNotification(str(pe)) def _entity_config_from_profile(self, table_name, short_name): """ Creates an EntityConfig object from the table name. :param table_name: Name of the database table. :type table_name: str :return: Entity configuration object. :rtype: EntityConfig """ table_display_name = format_name(short_name) entity = self.curr_profile.entity_by_name(table_name) model = entity_model(entity) if model is not None: # Entity configuration entity_cfg = EntityConfiguration() entity_cfg.Title = table_display_name entity_cfg.STRModel = model entity_cfg.data_source_name = table_name for col, factory in self._get_widget_factory(entity): entity_cfg.LookupFormatters[col.name] = factory # Load filter and display columns # using only those which are of # numeric/varchar type searchable_columns = entity_searchable_columns(entity) display_columns = entity_display_columns(entity) for c in searchable_columns: if c != 'id': entity_cfg.filterColumns[c] = format_name(c) for c in display_columns: if c != 'id': entity_cfg.displayColumns[c] = format_name(c) return entity_cfg else: return None def _get_widget_factory(self, entity): """ Get widget factory for specific column type :param entity: Current column entity object :type entity: Entity :return c: Column object corresponding to the widget factory :rtype c: BaseColumn :return col_factory: Widget factory corresponding to the column type :rtype col_factory: ColumnWidgetRegistry """ for c in entity.columns.values(): col_factory = ColumnWidgetRegistry.factory(c.TYPE_INFO) if col_factory is not None: yield c, col_factory(c) def add_entity_config(self, config): """ Set an entity configuration option and add it to the 'Search Entity' tab. """ entityWidg = STRViewEntityWidget(config) entityWidg.asyncStarted.connect(self._progressStart) entityWidg.asyncFinished.connect(self._progressFinish) tabIndex = self.tbSTREntity.addTab(entityWidg, config.Title) return entityWidg def entityTabIndexChanged(self, index): """ Raised when the tab index of the entity search tab widget changes. """ # Get the current widget in the tab container entityWidget = self.tbSTREntity.currentWidget() if isinstance(entityWidget, EntitySearchItem): entityWidget.loadAsync() def searchEntityRelations(self): """ Slot that searches for matching items for the specified entity and corresponding STR entities. """ entityWidget = self.tbSTREntity.currentWidget() entity_name = entityWidget.config.data_source_name self._reset_controls() if isinstance(entityWidget, EntitySearchItem): valid, msg = entityWidget.validate() if not valid: self._notif_search_config.clear() self._notif_search_config.insertErrorNotification(msg) return results, searchWord = entityWidget.executeSearch() # Show error message if len(results) == 0: noResultsMsg = QApplication.translate( 'ViewSTR', 'No results found for "{}"'.format(searchWord)) self._notif_search_config.clear() self._notif_search_config.insertErrorNotification(noResultsMsg) return party_names = [ e.name for e in self.curr_profile.social_tenure.parties ] entity = self.curr_profile.entity_by_name(entity_name) result_ids = [r.id for r in results] if entity_name in party_names: self.active_spu_id = self.details_tree_view.search_party( entity, result_ids) else: self.details_tree_view.search_spatial_unit(entity, result_ids) # self.tbPropertyPreview._iface.activeLayer().selectByExpression("id={}".format(self.active_spu_id)) # self.details_tree_view._selected_features = self.tbPropertyPreview._iface.activeLayer().selectedFeatures() # self._load_root_node(entity_name, formattedNode) def clearSearch(self): """ Clear search input parameters (for current widget) and results. """ entityWidget = self.tbSTREntity.currentWidget() if isinstance(entityWidget, EntitySearchItem): entityWidget.reset() self._reset_controls() def _reset_controls(self): # Clear tree view self._resetTreeView() # Clear document listings self._deleteSourceDocTabs() # Remove spatial unit memory layer self.tbPropertyPreview.remove_layer() def on_select_results(self): """ Slot which is raised when the selection is changed in the tree view selection model. """ if len(self.details_tree_view.view.selectedIndexes()) < 1: self.disable_buttons() return self.search_done = True index = self.details_tree_view.view.selectedIndexes()[0] item = self.details_tree_view.model.itemFromIndex(index) QApplication.processEvents() # STR node - edit social tenure relationship if item.text() == self.details_tree_view.str_text: entity = self.curr_profile.social_tenure str_model = self.details_tree_view.str_models[item.data()] self.details_tree_view.selected_model = str_model self.details_tree_view.selected_item = SelectedItem(item) documents = self.details_tree_view._supporting_doc_models( entity.name, str_model) self._load_source_documents(documents) # if there is supporting document, # expand supporting document tab if len(documents) > 0: self.toolBox.setCurrentIndex(1) self.disable_buttons(False) # party node - edit party elif item.data() in self.details_tree_view.spatial_unit_items.keys(): self.toolBox.setCurrentIndex(0) entity = self.details_tree_view.spatial_unit_items[item.data()] model = self.details_tree_view.feature_model(entity, item.data()) self.draw_spatial_unit(entity.name, model) self.disable_buttons() canvas = iface.mapCanvas() if canvas: canvas.zoomToFullExtent() else: self.disable_buttons() def disable_buttons(self, status=True): if self._can_edit: self.deleteSTR.setDisabled(status) if self._can_delete: self.editSTR.setDisabled(status) def str_party_column_obj(self, record): """ Gets the current party column name in STR table by finding party column with value other than None. :param record: The STR record or result. :type record: Dictionary :return: The party column name with value. :rtype: String """ for party in self.parties: party_name = party.short_name.lower() party_id = '{}_id'.format(party_name) if party_id not in record.__dict__: return None if record.__dict__[party_id] != None: party_id_obj = getattr(self.str_model, party_id) return party_id_obj def load_edit_str_editor(self): self.details_tree_view.edit_selected_node(self.details_tree_view) self.btnSearch.click() self.disable_buttons() def load_new_str_editor(self): try: # Check type of node and perform corresponding action add_str = STREditor() add_str.exec_() except DummyException as ex: QMessageBox.critical( self._plugin.iface.mainWindow(), QApplication.translate("STDMPlugin", "Loading Error"), str(ex)) def delete_str(self): self.details_tree_view.delete_selected_item() self.btnSearch.click() self.disable_buttons() def onSourceDocumentRemoved(self, container_id, doc_uuid, removed_doc): """ Slot raised when a source document is removed from the container. If there are no documents in the specified container then remove the tab. """ curr_container = self.tbSupportingDocs.currentWidget() curr_doc_widget = curr_container.findChildren(DocumentWidget) for doc in curr_doc_widget: if doc.fileUUID == doc_uuid: doc.deleteLater() self.removed_docs = removed_doc def draw_spatial_unit(self, entity_name, model): """ Render the geometry of the given spatial unit in the spatial view. :param row_id: Sqlalchemy object representing a feature. """ entity = self.curr_profile.entity_by_name(entity_name) self.tbPropertyPreview.draw_spatial_unit(entity, model) def showEvent(self, event): """ (Re)load map layers in the viewer and main canvas. :param event: Window event :type event: QShowEvent """ self.setEnabled(True) if QTimer is not None: QTimer.singleShot(200, self.init_mirror_map) return QMainWindow.showEvent(self, event) def init_mirror_map(self): self._notify_no_base_layers() # Add spatial unit layer if it doesn't exist self.tbPropertyPreview.refresh_canvas_layers() self.tbPropertyPreview.load_web_map() def _notify_no_base_layers(self): """ Checks if there are any base layers that will be used when visualizing the spatial units. If there are no base layers then insert warning message. """ self._notif_search_config.clear() num_layers = len(QgsProject.instance().mapLayers()) if num_layers == 0: msg = QApplication.translate( "ViewSTR", "No basemap layers are loaded in the " "current project. Basemap layers " "enhance the visualization of spatial units.") self._notif_search_config.insertWarningNotification(msg) def _deleteSourceDocTabs(self): """ Removes all source document tabs and deletes their references. """ tabCount = self.tbSupportingDocs.count() while tabCount != 0: srcDocWidget = self.tbSupportingDocs.widget(tabCount - 1) self.tbSupportingDocs.removeTab(tabCount - 1) del srcDocWidget tabCount -= 1 self._strID = None self._source_doc_manager.reset() def _resetTreeView(self): """ Clears the results tree view. """ # Reset tree view strModel = self.details_tree_view.view.model() resultsSelModel = self.details_tree_view.view.selectionModel() if strModel: strModel.clear() if resultsSelModel: if self.search_done: resultsSelModel.selectionChanged.disconnect( self.on_select_results) resultsSelModel.selectionChanged.connect(self.on_select_results) def _load_source_documents(self, source_docs): """ Load source documents into document listing widget. """ # Configure progress dialog progress_msg = QApplication.translate( "ViewSTR", "Loading supporting documents...") progress_dialog = QProgressDialog(self) if len(source_docs) > 0: progress_dialog.setWindowTitle(progress_msg) progress_dialog.setRange(0, len(source_docs)) progress_dialog.setWindowModality(Qt.WindowModal) progress_dialog.setFixedWidth(380) progress_dialog.show() progress_dialog.setValue(0) self._notif_search_config.clear() self.tbSupportingDocs.clear() self._source_doc_manager.reset() if len(source_docs) < 1: empty_msg = QApplication.translate( 'ViewSTR', 'No supporting document is uploaded ' 'for this social tenure relationship.') self._notif_search_config.clear() self._notif_search_config.insertWarningNotification(empty_msg) for i, (doc_type_id, doc_obj) in enumerate(source_docs.items()): # add tabs, and container and widget for each tab tab_title = self._source_doc_manager.doc_type_mapping[doc_type_id] tab_widget = QWidget() tab_widget.setObjectName(tab_title) cont_layout = QVBoxLayout(tab_widget) cont_layout.setObjectName('widget_layout_' + tab_title) scrollArea = QScrollArea(tab_widget) scrollArea.setFrameShape(QFrame.NoFrame) scrollArea_contents = QWidget() scrollArea_contents.setObjectName('tab_scroll_area_' + tab_title) tab_layout = QVBoxLayout(scrollArea_contents) tab_layout.setObjectName('layout_' + tab_title) scrollArea.setWidgetResizable(True) scrollArea.setWidget(scrollArea_contents) cont_layout.addWidget(scrollArea) self._source_doc_manager.registerContainer(tab_layout, doc_type_id) for doc in doc_obj: try: # add doc widgets self._source_doc_manager.insertDocFromModel( doc, doc_type_id) except DummyException as ex: LOGGER.debug(str(ex)) self.tbSupportingDocs.addTab(tab_widget, tab_title) progress_dialog.setValue(i + 1) # def _on_node_reference_changed(self, rootHash): # """ # Method for resetting document listing and map preview # if another root node and its children # are selected then the documents are reset as # well as the map preview control. # """ # if rootHash != self._curr_rootnode_hash: # self._deleteSourceDocTabs() # self._curr_rootnode_hash = rootHash def _progressStart(self): """ Load progress dialog window. For items whose durations is unknown, 'isindefinite' = True by default. If 'isindefinite' is False, then 'rangeitems' has to be specified. """ pass def _progressFinish(self): """ Hide progress dialog window. """ pass def _edit_permissions(self): """ Returns True/False whether the current logged in user has permissions to create new social tenure relationships. If true, then the system assumes that they can also edit STR records. """ canEdit = False userName = globals.APP_DBCONN.User.UserName authorizer = Authorizer(userName) newSTRCode = "9576A88D-C434-40A6-A318-F830216CA15A" # Get the name of the content from the code cnt = Content() createSTRCnt = cnt.queryObject().filter( Content.code == newSTRCode).first() if createSTRCnt: name = createSTRCnt.name canEdit = authorizer.CheckAccess(name) return canEdit
class AdminUnitManager(WIDGET, BASE): ''' Administrative Unit Manager Widget ''' # Signal raised when the state (view/manage) of the widet changes. stateChanged = pyqtSignal('bool') def __init__(self, parent=None, State=VIEW): QWidget.__init__(self, parent) self.setupUi(self) self.btnRemove.setIcon(GuiUtils.get_icon('remove.png')) self.btnClear.setIcon(GuiUtils.get_icon('reset.png')) self.btnAdd.setIcon(GuiUtils.get_icon('add.png')) self._defaultEditTriggers = self.tvAdminUnits.editTriggers() self._state = State self._onStateChange() self._notifBar = NotificationBar(self.vlNotification) # Configure validating line edit controls # invalidMsg = "{} already exists." # self.txtUnitCode.setModelAttr(AdminSpatialUnitSet,"Code") # self.txtUnitCode.setInvalidMessage(invalidMsg) # self.txtUnitCode.setNotificationBar(self._notifBar) ''' Initialize formatter for the rendering the admin unit nodes and insert the root node into the tree view model. ''' self._adminUnitNodeFormatter = AdminUnitFormatter( self.tvAdminUnits, self) self._rtNode = self._adminUnitNodeFormatter.rootNode self._adminUnitTreeModel = STRTreeViewModel( self._adminUnitNodeFormatter.root(), view=self.tvAdminUnits) self.tvAdminUnits.setModel(self._adminUnitTreeModel) self.tvAdminUnits.hideColumn(2) self.tvAdminUnits.setColumnWidth(0, 220) self.tvAdminUnits.expandAll() # Connects slots self.btnAdd.clicked.connect(self.onCreateAdminUnit) self.btnClear.clicked.connect(self.onClearSelection) self.btnRemove.clicked.connect(self.onDeleteSelection) self._adminUnitTreeModel.dataChanged.connect(self.onModelDataChanged) def model(self): """ :return: Returns the model associated with the administrative unit view. :rtype: STRTreeViewModel """ return self._adminUnitTreeModel def selection_model(self): """ :return: Returns the selection model associated with the administrative unit tree view. :rtype: QItemSelectionModel """ return self.tvAdminUnits.selectionModel() def state(self): ''' Returns the current state that the widget has been configured in. ''' return self._state def setState(self, state): ''' Set the state of the widget. ''' self._state = state self._onStateChange() def notificationBar(self): ''' Returns the application notification widget. ''' return self._notifBar def _onStateChange(self): ''' Configure controls upon changing the state of the widget. ''' manageControls = False if self._state == VIEW else True self.btnRemove.setVisible(manageControls) self.btnClear.setVisible(manageControls) self.gbManage.setVisible(manageControls) if manageControls: self.tvAdminUnits.setEditTriggers(self._defaultEditTriggers) else: self.tvAdminUnits.setEditTriggers(QAbstractItemView.NoEditTriggers) self.stateChanged.emit(manageControls) def onCreateAdminUnit(self): ''' Slot raised on clicking to add a new administrative unit. ''' self._notifBar.clear() if self.txtUnitName.text() == "": msg = QApplication.translate( "AdminUnitManager", "Name of the administrative unit cannot be empty.") self._notifBar.insertErrorNotification(msg) self.txtUnitName.setFocus() return if not self.txtUnitName.validate(): return if self.txtUnitCode.text() == "": msg = QApplication.translate( "AdminUnitManager", "Code of the administrative unit cannot be empty.") self._notifBar.insertErrorNotification(msg) self.txtUnitCode.setFocus() return # if not self.txtUnitCode.validate(): # return # Get current row selection selIndexes = self.tvAdminUnits.selectionModel().selectedRows(0) if len(selIndexes) == 0: # Get the number of items in the tree view rootIndex = self.tvAdminUnits.rootIndex() rowCount = self._adminUnitTreeModel.rowCount(rootIndex) if rowCount > 0: msg = QApplication.translate("AdminUnitManager", "You have not selected any parent node for the new administrative unit. Do " \ "you want to add it as one of the topmost administrative units?\nClick Yes to " \ "proceed or No to cancel.") selOption = QMessageBox.warning( self, QApplication.translate("AdminUnitManager", "No Parent Node Selected"), msg, QMessageBox.Yes | QMessageBox.No) if selOption == QMessageBox.Yes: parentNode = self._rtNode # We are interested in the model index of the root node parentModelIndex = rootIndex parentModel = None else: return # Do not prompt user and immediately add the administrative unit to the root node. else: parentNode = self._rtNode parentModelIndex = rootIndex parentModel = None else: # Get model index for the first column as this is where the new node will be added as the child parentModelIndex = selIndexes[0] parentNode = self._adminUnitTreeModel._getNode(parentModelIndex) parentID = parentNode.data(2) ausModel = AdminSpatialUnitSet() parentModel = ausModel.queryObject().filter( AdminSpatialUnitSet.id == parentID).first() adminUnitModel = AdminSpatialUnitSet(self.txtUnitName.text(), self.txtUnitCode.text(), parentModel) # Commit transaction to the database adminUnitModel.save() # Extract properties from the model ausProps = self._adminUnitNodeFormatter._extractAdminUnitSetInfo( adminUnitModel) childNode = BaseSTRNode(ausProps, parentNode) # Insert row into the view self._adminUnitTreeModel.insertRows(parentNode.childCount(), 1, parentModelIndex) self.clearInputs() def onClearSelection(self): ''' Slot that removes any existing selections in the tree view. ''' self.tvAdminUnits.selectionModel().clearSelection() def onModelDataChanged(self, oldindex, newindex): ''' Slot raised when the model data is changed. ''' # Get model index containing ID property refNode = self._adminUnitTreeModel._getNode(newindex) ausID = refNode.data(2) ausHandler = AdminSpatialUnitSet() ausObj = ausHandler.queryObject().filter( AdminSpatialUnitSet.id == ausID).first() if ausObj != None: attrColumn = newindex.column() if attrColumn == 0: ausObj.Name = refNode.data(0) elif attrColumn == 1: ausObj.Code = refNode.data(1) ausObj.update() def onDeleteSelection(self): ''' Slot raised to delete current selection of administrative unit. ''' self._notifBar.clear() # Get current row selection selIndexes = self.tvAdminUnits.selectionModel().selectedRows(2) if len(selIndexes) == 0: msg = QApplication.translate( "AdminUnitManager", "Please select the administrative unit to delete.") self._notifBar.insertWarningNotification(msg) else: delmsg = QApplication.translate("AdminUnitManager", "This action will delete the selected administrative unit plus any " \ "existing children under it. It cannot be undone.\nClick Yes to " \ "delete or No to cancel.") selOption = QMessageBox.warning( self, QApplication.translate("AdminUnitManager", "Confirm deletion"), delmsg, QMessageBox.Yes | QMessageBox.No) if selOption == QMessageBox.Yes: # Get the node in the current selection delIndex = selIndexes[0] ausNode = self._adminUnitTreeModel._getNode(delIndex) ausId = ausNode.data(2) ausHandler = AdminSpatialUnitSet() ausObj = ausHandler.queryObject().filter( AdminSpatialUnitSet.id == ausId).first() if not ausObj is None: ausObj.delete() # Remove item in tree view self._adminUnitTreeModel.removeRows( delIndex.row(), 1, delIndex.parent()) # Notify user self._notifBar.clear() successmsg = QApplication.translate( "AdminUnitManager", "Administrative unit successfully deleted.") self._notifBar.insertSuccessNotification(successmsg) def selectedAdministrativeUnit(self): ''' Returns the selected administrative unit object otherwise, if there is no selection then it returns None. ''' selIndexes = self.tvAdminUnits.selectionModel().selectedRows(2) if len(selIndexes) == 0: selAdminUnit = None else: selIndex = selIndexes[0] ausNode = self._adminUnitTreeModel._getNode(selIndex) ausId = ausNode.data(2) ausHandler = AdminSpatialUnitSet() selAdminUnit = ausHandler.queryObject().filter( AdminSpatialUnitSet.id == ausId).first() return selAdminUnit def clearInputs(self): ''' Clears the input controls. ''' self.txtUnitCode.clear() self.txtUnitName.clear()
class ImageExportSettings(QDialog, Ui_ImageExportSettings): """A dialog for settings options for exporting an image.""" def __init__(self, parent=None, **kwargs): super(ImageExportSettings, self).__init__(parent) self.setupUi(self) self._image_tr = self.tr('Image') self.notif_bar = NotificationBar(self.vl_notification, 6000) #Connect signals self.btn_path.clicked.connect(self._on_choose_image_path) self.buttonBox.accepted.connect(self.on_accept) self.sb_resolution.valueChanged.connect(self._on_resolution_changed) #Set color button defaults self._default_color = Qt.white self.btn_color.setDefaultColor(self._default_color) self.btn_color.setColor(self._default_color) self.btn_color.setAllowAlpha(True) self.path = kwargs.get('image_path', '') self.resolution = kwargs.get('resolution', '96') self.background_color = kwargs.get('background', Qt.transparent) self._update_controls() def _update_controls(self): #Update input controls with export settings' values if self.background_color == Qt.transparent: self.rb_transparent.setChecked(True) self.btn_color.setVisible(False) else: self.rb_fill.setChecked(True) self.btn_color.setColor(self.background_color) self.sb_resolution.setValue(int(self.resolution)) self.txt_path.setText(self.path) #Set image size just in case value does not change self._set_image_size() def _on_resolution_changed(self, value): #Slot raised when the resolution changes self._set_image_size() def _update_export_vars(self): #Update export variables based on user values. self.path = self.txt_path.text() self.resolution = self.sb_resolution.value() if self.rb_transparent.isChecked(): self.background_color = Qt.transparent else: self.background_color = self.btn_color.color() def _set_image_size(self): #Set image size based on the resolution using A4 paper size res = self.sb_resolution.value() if res == 0: return #To mm res_mm = res / 25.4 #A4 landscape size width = int(297 * res_mm) height = int(210 * res_mm) units = 'px' width_display = u'{0} {1}'.format(width, units) height_display = u'{0} {1}'.format(height, units) self.lbl_width.setText(width_display) self.lbl_height.setText(height_display) def _image_filters(self): #Return supported image formats for use in a QFileDialog filter formats = [] for f in QImageWriter.supportedImageFormats(): f_type = f.data() filter_format = u'{0} {1} (*.{2})'.format(f_type.upper(), self._image_tr, f_type) formats.append(filter_format) return ';;'.join(formats) def _on_choose_image_path(self): #Slot raised to choose image path img_path = self.txt_path.text() title = self.tr('Specify image location') sel_image_path = QFileDialog.getSaveFileName(self, title, img_path, self._image_filters()) if sel_image_path: self.txt_path.setText(sel_image_path) def on_accept(self): """ Slot raised to save the settings and close the dialog. """ if not self.txt_path.text(): msg = self.tr('Please specify the image path.') self.notif_bar.insertErrorNotification(msg) self.txt_path.setFocus() return self._update_export_vars() self.accept()
class LookupEditor(QDialog, Ui_dlgLookup): """ Form to add/edit lookup entities. """ def __init__(self, parent, profile, lookup=None): """ :param parent: Owner of this dialog :type parent: QWidget :param profile: A profile to add/edit lookup :type profile: Profile :type inplace: Flag to check if lookup creation is initiated from the 'normal' lookup creation process -inplace = False, this is the normal state. If 'inplace' = True, then creation is initiated from the the lookup selection dialog :param lookup: Value list to create, if None this is a new value list else its an edit :type lookup: ValueList """ QDialog.__init__(self, parent) self.setupUi(self) self.profile = profile self.lookup = lookup self.notice_bar = NotificationBar(self.notif_bar) self.init_gui() def init_gui(self): """ Initializes form widgets """ self.edtName.setFocus() if self.lookup: self.edtName.setText(self.lookup.short_name.replace('check_', '')) self.edtName.textChanged.connect(self.validate_text) def show_notification(self, message): """ Shows a warning notification bar message. :param message: The message of the notification. :type message: String """ msg = self.tr(message) self.notice_bar.clear() self.notice_bar.insertErrorNotification(msg) def validate_text(self, text): """ Validates and updates the entered text if necessary. Spaces are replaced by _ and capital letters are replaced by small. :param text: The text entered :type text: String """ text_edit = self.sender() cursor_position = text_edit.cursorPosition() text_edit.setValidator(None) if len(text) == 0: return name_regex = QtCore.QRegExp('^(?=.{0,40}$)[ _a-zA-Z][a-zA-Z0-9_ ]*$') name_validator = QtGui.QRegExpValidator(name_regex) text_edit.setValidator(name_validator) QApplication.processEvents() last_character = text[-1:] state = name_validator.validate(text, text.index(last_character))[0] if state != QValidator.Acceptable: self.show_notification( '"{}" is not allowed at this position.'.format(last_character)) text = text[:-1] else: # fix caps, underscores, and spaces if last_character.isupper(): text = text.lower() if last_character == ' ': text = text.replace(' ', '_') if len(text) > 1: if text[0] == ' ' or text[0] == '_': text = text[1:] text = text.replace(' ', '_').lower() self.blockSignals(True) text_edit.setText(text) text_edit.setCursorPosition(cursor_position) self.blockSignals(False) text_edit.setValidator(None) def format_lookup_name(self, name): """ Replace spaces with underscore in a name string :param name: Name to replace spaces :type name: str :rtype: str """ formatted_name = str(name).strip() formatted_name = formatted_name.replace(' ', "_") return formatted_name.lower() def create_lookup(self, name): """ Creates a lookup entity and add it to a profile. If this is an edit, first the previous lookup is removed before adding a new one. :param name: Name of the new/edited lookup :type name: Unicode """ name = self.format_lookup_name(name) new_lookup = self.profile.create_entity(name, value_list_factory) return new_lookup def accept(self): if self.edtName.text() == '' or self.edtName.text() == '_': self.error_message( QApplication.translate("LookupEditor", "Please enter a valid lookup name.")) return if self.edtName.text() == 'check': self.error_message( QApplication.translate( "LookupEditor", "'check' is used internally by STDM! " "Select another name for the lookup")) return short_name = unicode(self.edtName.text()) if self.lookup is None: # new lookup if self.duplicate_check(short_name): self.show_message( self.tr("Lookup with the same name already " "exist in the current profile!")) return else: new_lookup = self.create_lookup(short_name) self.profile.add_entity(new_lookup) self.lookup = new_lookup else: self.edit_lookup(short_name) self.done(1) def duplicate_check(self, name): """ Return True if we have an entity in the current profile with same 'name' :param name: entity short_name :type name: Unicode :rtype:boolean """ return self.profile.entities.has_key(name) def edit_lookup(self, short_name): short_name = short_name.replace('check_', '') self.lookup.short_name = u'{0}_{1}'.format('check', short_name) def reject(self): self.done(0) def error_message(self, message): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("STDM") msg.setText(message) msg.exec_()
class MultipleEnumerationDialog(WIDGET, BASE, TranslatorDialogBase): """ Dialog for defining configuration settings for the MultipleEnumerationTranslator class implementation. """ def __init__(self, parent, source_cols, dest_table, dest_col, src_col): QDialog.__init__(self, parent) self.setupUi(self) TranslatorDialogBase.__init__(self, source_cols, dest_table, dest_col, src_col) self._notif_bar = NotificationBar(self.vl_notification) # Container of column names for an enumeration table self._enum_col_names = [] self._load_separators() # Init user selection to the corresponding UI controls self.txt_source_col.setText(self._src_col) def _load_separators(self): separators = [] comma_sep = (QApplication.translate("MultipleEnumerationDialog", "Comma (,)"), ",") separators.append(comma_sep) colon_sep = (QApplication.translate("MultipleEnumerationDialog", "Colon (:)"), ":") separators.append(colon_sep) semi_colon_sep = (QApplication.translate("MultipleEnumerationDialog", "Semi-colon (;)"), ";") separators.append(semi_colon_sep) asterisk_sep = (QApplication.translate("MultipleEnumerationDialog", "Asterisk (*)"), "*") separators.append(asterisk_sep) self.cbo_separator.addItem("") for sep in separators: self.cbo_separator.addItem(sep[0], sep[1]) def validate(self): """ :return: Check user entries. :rtype: bool """ if not self.txt_source_col.text(): msg = QApplication.translate("MultipleEnumerationDialog", "Source column does not exist.") self._notif_bar.clear() self._notif_bar.insertErrorNotification(msg) return False if not self.cbo_separator.currentText(): msg = QApplication.translate("MultipleEnumerationDialog", "Please specify a separator for the multiple select data.") self._notif_bar.clear() self._notif_bar.insertErrorNotification(msg) return False return True def separator(self): sep_idx = self.cbo_separator.currentIndex() if sep_idx == 0: return "" else: return self.cbo_separator.itemData(sep_idx) def column_pairings(self): """ Format of dict - source column name: matching enumeration table column. Primary enum column is the first item in the dictionary. """ col_pairs = OrderedDict() col_pairs[self.txt_source_col.text()] = self._dest_col return col_pairs def value_translator(self): enum_translator = MultipleEnumerationTranslator() enum_translator.set_referencing_table(self._dest_table) enum_translator.set_referencing_column(self._dest_col) enum_translator.set_input_referenced_columns(self.column_pairings()) enum_translator.set_separator(self.separator()) return enum_translator def accept(self): """ Validate before accepting user input. """ if self.validate(): super(MultipleEnumerationDialog, self).accept()
class GeoODKConverter(QDialog, FORM_CLASS): def __init__(self, parent=None): """Class Constructor.""" super(GeoODKConverter, self).__init__(parent) # Set up the user interface from Designer. # After setupUI you can access any designer object by doing # self.<objectname>, and you can use autoconnect slots - see # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html # #widgets-and-dialogs-with-aut o-connect self.connect_action = pyqtSignal(str) self.setupUi(self) self.chk_all.setCheckState(Qt.Checked) self.entity_model = EntitiesModel() self.set_entity_model_view(self.entity_model) self.stdm_config = None self.parent = parent self.load_profiles() self.check_state_on() self.check_geoODK_path_exist() self.chk_all.stateChanged.connect(self.check_state_on) self.btnShowOutputFolder.clicked.connect(self.onShowOutputFolder) # self.btn_upload.clicked.connect(self.upload_generated_form) self._notif_bar_str = NotificationBar(self.vlnotification) def onShowOutputFolder(self): output_path = FORM_HOME # windows if sys.platform.startswith('win32'): os.startfile(output_path) # *nix systems if sys.platform.startswith('linux'): subprocess.Popen(['xdg-open', output_path]) # macOS if sys.platform.startswith('darwin'): subprocess.Popen(['open', output_path]) def check_state_on(self): """ Ensure all the items in the list are checked :return: """ if self.entity_model.rowCount() > 0: for row in range(self.entity_model.rowCount()): item = self.entity_model.item(row) if self.chk_all.isChecked(): item.setCheckState(Qt.Checked) else: item.setCheckState(Qt.Unchecked) def load_profiles(self): """ Read and load profiles from StdmConfiguration instance """ self.populate_view_models(current_profile()) def profiles(self): """ Get all profiles :return: """ return list(self.load_config().values()) def populate_view_models(self, profile): for entity in profile.entities.values(): if entity.action == DbItem.DROP: continue if hasattr(entity, 'user_editable') and entity.TYPE_INFO != 'VALUE_LIST': if entity.user_editable == False: continue if entity.TYPE_INFO not in [ 'SUPPORTING_DOCUMENT', 'SOCIAL_TENURE', 'ADMINISTRATIVE_SPATIAL_UNIT', 'ENTITY_SUPPORTING_DOCUMENT', 'ASSOCIATION_ENTITY', 'AUTO_GENERATE_CODE' ]: if entity.TYPE_INFO == 'VALUE_LIST': pass else: self.entity_model.add_entity(entity) self.set_model_items_selectable() def set_entity_model_view(self, entity_model): """ Set our list view to the default model :return: """ self.trentities.setModel(entity_model) def set_model_items_selectable(self): """ Ensure that the entities are checkable :return: """ if self.entity_model.rowCount() > 0: for row in range(self.entity_model.rowCount()): index = self.entity_model.index(row, 0) item_index = self.entity_model.itemFromIndex(index) item_index.setCheckable(True) def selected_entities_from_Model(self): """ Get selected entities for conversion to Xform from the user selection :return: """ entity_list = [] if self.entity_model.rowCount() > 0: for row in range(self.entity_model.rowCount()): item = self.entity_model.item(row) if item.isCheckable() and item.checkState() == Qt.Checked: entity_list.append(item.text()) return entity_list def check_geoODK_path_exist(self): """ Check if the geoodk paths are there in the directory Otherwise create them :return: """ if not os.access(FORM_HOME, os.F_OK): os.makedirs(str(FORM_HOME)) def upload_generated_form(self): """ Upload the generated Xform file to mobile phone. This eliminates the process of copying the file manually to the mobile device :return: """ from stdm.ui.geoodk_mobile_upload import FormUploader form_uploader = FormUploader(self) form_uploader.exec_() def generate_mobile_form(self, selected_entities): """ Generate mobile form based on the selected entities. :return: """ # try: self._notif_bar_str.clear() if len(selected_entities) == 0: self._notif_bar_str.insertErrorNotification( 'No entity selected. Please select at least one entity...') return if len(selected_entities) > 0: geoodk_writer = GeoodkWriter(selected_entities, self.str_supported) geoodk_writer.write_data_to_xform() msg = 'File saved in: {}' self._notif_bar_str.insertInformationNotification( msg.format(FORM_HOME)) # except DummyException as ex: # self._notif_bar_str.insertErrorNotification(ex.message + # ': Unable to generate Mobile Form') # return def accept(self): """ Generate mobile forms based on user selected entities. Check if str is enabled, then ensure str tables are enabled. :return: """ user_entities = self.selected_entities_from_Model() self.str_supported = False if self.ck_social_tenure.isChecked(): self.str_supported = True str_definition = current_profile().social_tenure str_definition_party = str_definition.parties[0].short_name str_definition_spatial = str_definition.spatial_units[0].short_name if str_definition_party not in user_entities or str_definition_spatial not in user_entities: self._notif_bar_str.insertErrorNotification( 'One of the entities required to define str is not selected. Form not saved' ) return # else: # self.generate_mobile_form(user_entities) # else: self.generate_mobile_form(user_entities)
class ColumnEditor(QDialog, Ui_ColumnEditor): """ Dialog to add/edit entity columns """ def __init__(self, **kwargs): """ :param parent: Owner of this dialog :type parent: QWidget :param kwargs: Keyword dictionary of the following parameters; column - Column you editing, None if its a new column entity - Entity you are adding the column to profile - Current profile in_db - Boolean flag to indicate if a column has been created in the database auto_add- True to automatically add a new column to the entity, default is False. """ self.form_parent = kwargs.get('parent', self) self.column = kwargs.get('column', None) self.entity = kwargs.get('entity', None) self.profile = kwargs.get('profile', None) self.in_db = kwargs.get('in_db', False) self.is_new = kwargs.get('is_new', True) self.auto_entity_add = kwargs.get('auto_add', False) QDialog.__init__(self, self.form_parent) self.FK_EXCLUDE = [u'supporting_document', u'admin_spatial_unit_set'] self.EX_TYPE_INFO = [ 'SUPPORTING_DOCUMENT', 'SOCIAL_TENURE', 'ADMINISTRATIVE_SPATIAL_UNIT', 'ENTITY_SUPPORTING_DOCUMENT', 'VALUE_LIST', 'ASSOCIATION_ENTITY', 'AUTO_GENERATED' ] self.setupUi(self) self.dtypes = {} self.type_info = '' # dictionary to hold default attributes for each data type self.type_attribs = {} self.init_type_attribs() # dictionary to act as a work area for the form fields. self.form_fields = {} self.init_form_fields() self.fk_entities = [] self.lookup_entities = [] # Exclude column type info in the list self._exclude_col_type_info = [] if self.is_new: self.prop_set = None # why not False?? else: self.prop_set = True # the current entity should not be part of the foreign key parent table, # add it to the exclusion list self.FK_EXCLUDE.append(self.entity.short_name) self.type_names = \ [unicode(name) for name in BaseColumn.types_by_display_name().keys()] self.cboDataType.currentIndexChanged.connect(self.change_data_type) self.btnColProp.clicked.connect(self.data_type_property) self.edtColName.textChanged.connect(self.validate_text) self.notice_bar = NotificationBar(self.notif_bar) self.init_controls() def exclude_column_types(self, type_info): """ Exclude the column types with the given type_info. :param type_info: List of TYPE_INFO of columns to exclude. :type type_info: list """ self._exclude_col_type_info = type_info # Block index change signal of combobox self.cboDataType.blockSignals(True) # Reload column data types self.populate_data_type_cbo() # Select column type if it had been specified if not self.column is None: text = self.column.display_name() self.cboDataType.setCurrentIndex(self.cboDataType.findText(text)) # Re-enable signals self.cboDataType.blockSignals(False) def show_notification(self, message): """ Shows a warning notification bar message. :param message: The message of the notification. :type message: String """ self.notice_bar.clear() self.notice_bar.insertErrorNotification(message) def init_controls(self): """ Initialize GUI controls default state when the dialog window is opened. """ self.populate_data_type_cbo() if not self.column is None: self.column_to_form(self.column) self.column_to_wa(self.column) self.edtColName.setFocus() self.edtColName.setEnabled(not self.in_db) self.cboDataType.setEnabled(not self.in_db) self.buttonBox.button(QtGui.QDialogButtonBox.Ok).clicked.connect( self.accept) self.buttonBox.button(QtGui.QDialogButtonBox.Cancel).clicked.connect( self.cancel) self.cbMandt.setEnabled(not self.in_db) self.cbUnique.setEnabled(not self.in_db) self.cbIndex.setEnabled(not self.in_db) def validate_text(self, text): """ Validates and updates the entered text if necessary. Spaces are replaced by _ and capital letters are replaced by small. :param text: The text entered :type text: String """ text_edit = self.sender() cursor_position = text_edit.cursorPosition() text_edit.setValidator(None) if len(text) == 0: return name_regex = QtCore.QRegExp('^(?=.{0,40}$)[ _a-zA-Z][a-zA-Z0-9_ ]*$') name_validator = QtGui.QRegExpValidator(name_regex) text_edit.setValidator(name_validator) QApplication.processEvents() last_character = text[-1:] locale = QSettings().value("locale/userLocale")[0:2] if locale == 'en': state = name_validator.validate(text, text.index(last_character))[0] if state != QValidator.Acceptable: self.show_notification( u'"{}" is not allowed at this position.'.format( last_character)) text = text[:-1] # fix caps, _, and spaces if last_character.isupper(): text = text.lower() if last_character == ' ': text = text.replace(' ', '_') if len(text) > 1: if text[0] == ' ' or text[0] == '_': text = text[1:] text = text.replace(' ', '_').lower() self.blockSignals(True) text_edit.setText(text) text_edit.setCursorPosition(cursor_position) self.blockSignals(False) text_edit.setValidator(None) def column_to_form(self, column): """ Initializes form controls with Column data. :param column: BaseColumn instance :type column: BaseColumn """ text = column.display_name() self.cboDataType.setCurrentIndex(self.cboDataType.findText(text)) self.edtColName.setText(column.name) self.edtColDesc.setText(column.description) self.txt_form_label.setText(column.label) self.edtUserTip.setText(column.user_tip) self.cbMandt.setChecked(column.mandatory) self.cbSearch.setCheckState(self.bool_to_check(column.searchable)) self.cbUnique.setCheckState(self.bool_to_check(column.unique)) self.cbIndex.setCheckState(self.bool_to_check(column.index)) ti = self.current_type_info() ps = self.type_attribs[ti].get('prop_set', None) if ps is not None: self.type_attribs[ti]['prop_set'] = self.prop_set def column_to_wa(self, column): """ Initialize 'work area' form_fields with column data. :param column: BaseColumn instance :type column: BaseColumn """ if column is not None: self.form_fields['colname'] = column.name self.form_fields['value'] = None self.form_fields['mandt'] = column.mandatory self.form_fields['search'] = column.searchable self.form_fields['unique'] = column.unique self.form_fields['index'] = column.index if hasattr(column, 'minimum'): self.form_fields['minimum'] = column.minimum self.form_fields['maximum'] = column.maximum if hasattr(column, 'srid'): self.form_fields['srid'] = column.srid self.form_fields['geom_type'] = column.geom_type if hasattr(column, 'entity_relation'): self.form_fields['entity_relation'] = column.entity_relation if hasattr(column, 'association'): self.form_fields[ 'first_parent'] = column.association.first_parent self.form_fields[ 'second_parent'] = column.association.second_parent if hasattr(column, 'min_use_current_date'): self.form_fields[ 'min_use_current_date'] = column.min_use_current_date self.form_fields[ 'max_use_current_date'] = column.max_use_current_date if hasattr(column, 'min_use_current_datetime'): self.form_fields['min_use_current_datetime'] = \ column.min_use_current_datetime self.form_fields['max_use_current_datetime'] = \ column.max_use_current_datetime if hasattr(column, 'prefix_source'): self.form_fields['prefix_source'] = column.prefix_source self.form_fields['leading_zero'] = column.leading_zero self.form_fields['separator'] = column.separator # Decimal properties if hasattr(column, 'precision'): self.form_fields['precision'] = column.precision self.form_fields['scale'] = column.scale def bool_to_check(self, state): """ Converts a boolean to a Qt checkstate. :param state: True/False :type state: boolean :rtype: Qt.CheckState """ return Qt.Checked if state else Qt.Unchecked def init_form_fields(self): """ Initializes work area 'form_fields' dictionary with default values. Used when creating a new column. """ none = QApplication.translate('CodeProperty', 'None') self.form_fields['colname'] = '' self.form_fields['value'] = None self.form_fields['mandt'] = False self.form_fields['search'] = False self.form_fields['unique'] = False self.form_fields['index'] = False self.form_fields['minimum'] = self.type_attribs.get('minimum', 0) self.form_fields['maximum'] = self.type_attribs.get('maximum', 0) self.form_fields['srid'] = self.type_attribs.get('srid', "") self.form_fields['geom_type'] = self.type_attribs.get('geom_type', 0) self.form_fields['in_db'] = self.in_db self.form_fields['prefix_source'] = self.type_attribs.get( 'prefix_source', none) self.form_fields['leading_zero'] = self.type_attribs.get( 'leading_zero', '') self.form_fields['separator'] = self.type_attribs.get('separator', '') self.form_fields['precision'] = self.type_attribs.get('precision', 18) self.form_fields['scale'] = self.type_attribs.get('scale', 6) self.form_fields['entity_relation'] = \ self.type_attribs['FOREIGN_KEY'].get('entity_relation', None) self.form_fields['entity_relation'] = \ self.type_attribs['LOOKUP'].get('entity_relation', None) self.form_fields['first_parent'] = \ self.type_attribs['MULTIPLE_SELECT'].get('first_parent', None) self.form_fields['second_parent'] = \ self.type_attribs['MULTIPLE_SELECT'].get('second_parent', None) self.form_fields['min_use_current_date'] = \ self.type_attribs['DATE'].get('min_use_current_date', None) self.form_fields['max_use_current_date'] = \ self.type_attribs['DATE'].get('max_use_current_date', None) self.form_fields['min_use_current_datetime'] = \ self.type_attribs['DATETIME'].get('min_use_current_datetime', None) self.form_fields['max_use_current_datetime'] = \ self.type_attribs['DATETIME'].get('max_use_current_datetime', None) def init_type_attribs(self): """ Initializes data type attributes. The attributes are used to set the form controls state when a particular data type is selected. mandt - enables/disables checkbox 'Mandatory' search - enables/disables checkbox 'Searchable' unique - enables/disables checkbox 'Unique' index - enables/disables checkbox 'Index' *property - function to execute when a data type is selected. """ self.type_attribs['VARCHAR'] = { 'mandt': { 'check_state': False, 'enabled_state': True }, 'search': { 'check_state': True, 'enabled_state': True }, 'unique': { 'check_state': False, 'enabled_state': True }, 'index': { 'check_state': False, 'enabled_state': True }, 'maximum': 30, 'property': self.varchar_property } self.type_attribs['INT'] = { 'mandt': { 'check_state': False, 'enabled_state': True }, 'search': { 'check_state': True, 'enabled_state': True }, 'unique': { 'check_state': False, 'enabled_state': True }, 'index': { 'check_state': False, 'enabled_state': False }, 'minimum': 0, 'maximum': 0, 'property': self.bigint_property } self.type_attribs['TEXT'] = { 'mandt': { 'check_state': False, 'enabled_state': True }, 'search': { 'check_state': False, 'enabled_state': False }, 'unique': { 'check_state': False, 'enabled_state': False }, 'index': { 'check_state': False, 'enabled_state': False }, } self.type_attribs['DOUBLE'] = { 'mandt': { 'check_state': False, 'enabled_state': True }, 'search': { 'check_state': True, 'enabled_state': True }, 'unique': { 'check_state': False, 'enabled_state': True }, 'index': { 'check_state': False, 'enabled_state': True }, 'minimum': 0.0, 'maximum': 0.0, 'precision': 18, 'scale': 6, 'property': self.double_property } self.type_attribs['DATE'] = { 'mandt': { 'check_state': False, 'enabled_state': True }, 'search': { 'check_state': False, 'enabled_state': True }, 'unique': { 'check_state': False, 'enabled_state': False }, 'index': { 'check_state': False, 'enabled_state': False }, 'minimum': datetime.date.min, 'maximum': datetime.date.max, 'min_use_current_date': False, 'max_use_current_date': False, 'property': self.date_property } self.type_attribs['DATETIME'] = { 'mandt': { 'check_state': False, 'enabled_state': True }, 'search': { 'check_state': False, 'enabled_state': True }, 'unique': { 'check_state': False, 'enabled_state': False }, 'index': { 'check_state': False, 'enabled_state': False }, 'minimum': datetime.datetime.min, 'maximum': datetime.datetime.max, 'min_use_current_datetime': False, 'max_use_current_datetime': False, 'property': self.dtime_property } self.type_attribs['FOREIGN_KEY'] = { 'mandt': { 'check_state': False, 'enabled_state': True }, 'search': { 'check_state': False, 'enabled_state': False }, 'unique': { 'check_state': False, 'enabled_state': False }, 'index': { 'check_state': False, 'enabled_state': False }, 'entity_relation': None, 'property': self.fk_property, 'prop_set': False } self.type_attribs['LOOKUP'] = { 'mandt': { 'check_state': False, 'enabled_state': True }, 'search': { 'check_state': True, 'enabled_state': True }, 'unique': { 'check_state': False, 'enabled_state': False }, 'index': { 'check_state': False, 'enabled_state': False }, 'entity_relation': {}, 'property': self.lookup_property, 'prop_set': False } self.type_attribs['GEOMETRY'] = { 'mandt': { 'check_state': False, 'enabled_state': False }, 'search': { 'check_state': False, 'enabled_state': False }, 'unique': { 'check_state': True, 'enabled_state': False }, 'index': { 'check_state': True, 'enabled_state': False }, 'srid': "", 'geom_type': 0, 'property': self.geometry_property, 'prop_set': False } self.type_attribs['BOOL'] = { 'mandt': { 'check_state': False, 'enabled_state': False }, 'search': { 'check_state': False, 'enabled_state': False }, 'unique': { 'check_state': False, 'enabled_state': False }, 'index': { 'check_state': False, 'enabled_state': False } } self.type_attribs['PERCENT'] = { 'mandt': { 'check_state': False, 'enabled_state': False }, 'search': { 'check_state': False, 'enabled_state': True }, 'unique': { 'check_state': False, 'enabled_state': False }, 'index': { 'check_state': False, 'enabled_state': False } } self.type_attribs['ADMIN_SPATIAL_UNIT'] = { 'mandt': { 'check_state': False, 'enabled_state': True }, 'search': { 'check_state': True, 'enabled_state': True }, 'unique': { 'check_state': False, 'enabled_state': False }, 'index': { 'check_state': False, 'enabled_state': False }, 'entity_relation': None } self.type_attribs['MULTIPLE_SELECT'] = { 'mandt': { 'check_state': False, 'enabled_state': True }, 'search': { 'check_state': False, 'enabled_state': True }, 'unique': { 'check_state': False, 'enabled_state': False }, 'index': { 'check_state': False, 'enabled_state': False }, 'first_parent': None, 'second_parent': self.entity, 'property': self.multi_select_property, 'prop_set': False } self.type_attribs['AUTO_GENERATED'] = { 'mandt': { 'check_state': False, 'enabled_state': True }, 'search': { 'check_state': True, 'enabled_state': True }, 'unique': { 'check_state': True, 'enabled_state': False }, 'index': { 'check_state': True, 'enabled_state': False }, 'prefix_source': '', 'leading_zero': '', 'separator': '', 'property': self.code_property, 'prop_set': True } def data_type_property(self): """ Executes the function assigned to the property attribute of the current selected data type. """ self.type_attribs[self.current_type_info()]['property']() def varchar_property(self): """ Opens the property editor for the Varchar data type. If successful, set a minimum column in work area 'form fields' """ editor = VarcharProperty(self, self.form_fields) result = editor.exec_() if result == 1: self.form_fields['maximum'] = editor.max_len() def bigint_property(self): """ Opens a property editor for the BigInt data type. """ editor = BigintProperty(self, self.form_fields) result = editor.exec_() if result == 1: self.form_fields['minimum'] = editor.min_val() self.form_fields['maximum'] = editor.max_val() def double_property(self): """ Opens a property editor for the Double data type. """ editor = DoubleProperty(self, self.form_fields) result = editor.exec_() if result == 1: self.form_fields['minimum'] = editor.min_val() self.form_fields['maximum'] = editor.max_val() def date_property(self): """ Opens a property editor for the Date data type. """ editor = DateProperty(self, self.form_fields) result = editor.exec_() if result == 1: self.form_fields['minimum'] = editor.min_val() self.form_fields['maximum'] = editor.max_val() self.form_fields['min_use_current_date'] = \ editor.min_use_current_date self.form_fields['max_use_current_date'] = \ editor.max_use_current_date def dtime_property(self): """ Opens a property editor for the DateTime data type. """ editor = DTimeProperty(self, self.form_fields) result = editor.exec_() if result == 1: self.form_fields['minimum'] = editor.min_val() self.form_fields['maximum'] = editor.max_val() self.form_fields['min_use_current_datetime'] = \ editor.min_use_current_datetime self.form_fields['max_use_current_datetime'] = \ editor.max_use_current_datetime def geometry_property(self): """ Opens a property editor for the Geometry data type. If successful, set the srid(projection), geom_type (LINE, POLYGON...) and prop_set which is boolean flag to verify that all the geometry properties are set. Constraint - If 'prop_set' is False column cannot be saved. """ editor = GeometryProperty(self, self.form_fields) result = editor.exec_() if result == 1: self.form_fields['srid'] = editor.coord_sys() self.form_fields['geom_type'] = editor.geom_type() self.property_set() def admin_spatial_unit_property(self): """ Sets entity relation property used when creating column of type ADMIN_SPATIAL_UNIT """ er_fields = {} er_fields['parent'] = self.entity er_fields['parent_column'] = None er_fields['display_columns'] = [] er_fields['child'] = None er_fields['child_column'] = None self.form_fields['entity_relation'] = EntityRelation( self.profile, **er_fields) def fk_property(self): """ Opens a property editor for the ForeignKey data type. """ if len(self.edtColName.displayText()) == 0: self.show_message("Please enter column name!") return # filter list of lookup tables, don't show internal # tables in list of lookups fk_ent = [entity for entity in self.profile.entities.items() \ if entity[1].TYPE_INFO not in self.EX_TYPE_INFO] fk_ent = [entity for entity in fk_ent if unicode(entity[0]) \ not in self.FK_EXCLUDE] relation = {} relation['form_fields'] = self.form_fields relation['fk_entities'] = fk_ent relation['profile'] = self.profile relation['entity'] = self.entity relation['column_name'] = unicode(self.edtColName.text()) editor = FKProperty(self, relation) result = editor.exec_() if result == 1: self.form_fields['entity_relation'] = editor.entity_relation() self.property_set() def lookup_property(self): """ Opens a lookup type property editor """ editor = LookupProperty(self, self.form_fields, profile=self.profile) result = editor.exec_() if result == 1: self.form_fields['entity_relation'] = editor.entity_relation() self.property_set() def multi_select_property(self): """ Opens a multi select property editor """ if len(self.edtColName.displayText()) == 0: self.show_message("Please enter column name!") return editor = MultiSelectProperty(self, self.form_fields, self.entity, self.profile) result = editor.exec_() if result == 1: self.form_fields['first_parent'] = editor.lookup() self.form_fields['second_parent'] = self.entity self.property_set() def code_property(self): """ Opens the code data type property editor """ editor = CodeProperty(self, self.form_fields, profile=self.profile) result = editor.exec_() if result == 1: self.form_fields['prefix_source'] = editor.prefix_source() self.form_fields['leading_zero'] = editor.leading_zero() self.form_fields['separator'] = editor.separator() self.property_set() def create_column(self): """ Creates a new BaseColumn. """ column = None if self.type_info <> "": if self.type_info == 'ADMIN_SPATIAL_UNIT': self.admin_spatial_unit_property() column = BaseColumn.registered_types[self.type_info] \ (self.form_fields['colname'], self.entity, **self.form_fields) return column if self.is_property_set(self.type_info): column = BaseColumn.registered_types[self.type_info] \ (self.form_fields['colname'], self.entity, self.form_fields['geom_type'], self.entity, **self.form_fields) else: self.show_message(self.tr('Please set column properties.')) return else: raise self.tr("No type to create.") return column def property_set(self): self.prop_set = True self.type_attribs[self.current_type_info()]['prop_set'] = True def is_property_set(self, ti): """ Checks if column property is set by reading the value of attribute 'prop_set' :param ti: Type info to check for prop set :type ti: BaseColumn.TYPE_INFO :rtype: boolean """ return self.type_attribs[ti].get('prop_set', True) def property_by_name(self, ti, name): try: return self.dtype_property(ti)['property'][name] except: return None def populate_data_type_cbo(self): """ Fills the data type combobox widget with BaseColumn type names. """ self.cboDataType.clear() for name, col in BaseColumn.types_by_display_name().iteritems(): # Specify columns to exclude if col.TYPE_INFO not in self._exclude_col_type_info: self.cboDataType.addItem(name) if self.cboDataType.count() > 0: self.cboDataType.setCurrentIndex(0) def change_data_type(self, index): """ Called by type combobox when you select a different data type. """ text = self.cboDataType.itemText(index) col_cls = BaseColumn.types_by_display_name().get(text, None) if col_cls is None: return ti = col_cls.TYPE_INFO if ti not in self.type_attribs: msg = self.tr('Column type attributes could not be found.') self.notice_bar.clear() self.notice_bar.insertErrorNotification(msg) return self.btnColProp.setEnabled(self.type_attribs[ti].has_key('property')) self.type_info = ti opts = self.type_attribs[ti] self.set_optionals(opts) self.set_min_max_defaults(ti) def set_optionals(self, opts): """ Enable/disables form controls based on selected column data type attributes param opts: Dictionary type properties of selected column type opts: dict """ self.cbMandt.setEnabled(opts['mandt']['enabled_state']) self.cbSearch.setEnabled(opts['search']['enabled_state']) self.cbUnique.setEnabled(opts['unique']['enabled_state']) self.cbIndex.setEnabled(opts['index']['enabled_state']) self.cbMandt.setCheckState( self.bool_to_check(opts['mandt']['check_state'])) self.cbSearch.setCheckState( self.bool_to_check(opts['search']['check_state'])) self.cbUnique.setCheckState( self.bool_to_check(opts['unique']['check_state'])) self.cbIndex.setCheckState( self.bool_to_check(opts['index']['check_state'])) def set_min_max_defaults(self, type_info): """ sets the work area 'form_fields' default values (minimum/maximum) from the column's type attribute dictionary :param type_info: BaseColumn.TYPE_INFO :type type_info: str """ self.form_fields['minimum'] = \ self.type_attribs[type_info].get('minimum', 0) self.form_fields['maximum'] = \ self.type_attribs[type_info].get('maximum', 0) def current_type_info(self): """ Returns a TYPE_INFO of a data type :rtype: str """ text = self.cboDataType.itemText(self.cboDataType.currentIndex()) try: return BaseColumn.types_by_display_name()[text].TYPE_INFO except: return '' def fill_work_area(self): """ Sets work area 'form_fields' with form control values """ self.form_fields['colname'] = unicode(self.edtColName.text()) self.form_fields['description'] = unicode(self.edtColDesc.text()) self.form_fields['label'] = unicode(self.txt_form_label.text()) self.form_fields['index'] = self.cbIndex.isChecked() self.form_fields['mandatory'] = self.cbMandt.isChecked() self.form_fields['searchable'] = self.cbSearch.isChecked() self.form_fields['unique'] = self.cbUnique.isChecked() self.form_fields['user_tip'] = unicode(self.edtUserTip.text()) def show_message(self, message): msg = QMessageBox() msg.setIcon(QMessageBox.Warning) msg.setWindowTitle(QApplication.translate("AttributeEditor", "STDM")) msg.setText(message) msg.exec_() def accept(self): col_name = unicode(self.edtColName.text()).strip() # column name is not empty if len(col_name) == 0 or col_name == '_': self.show_message(self.tr('Please enter a valid column name.')) return False # check for STDM reserved keywords if col_name in RESERVED_KEYWORDS: self.show_message( self.tr(u"'{0}' is a reserved keyword used internally by STDM.\n"\ "Please choose another column name.".format(col_name)) ) return False new_column = self.make_column() if new_column is None: LOGGER.debug("Error creating column!") self.show_message('Unable to create column!') return False if self.column is None: # new column if self.duplicate_check(col_name): self.show_message( self.tr("Column with the same name already " "exist in this entity!")) return False if self.auto_entity_add: self.entity.add_column(new_column) self.column = new_column self.done(1) else: # editing a column self.column = new_column self.done(1) def cancel(self): self.done(0) def make_column(self): """ Returns a newly created column :rtype: BaseColumn """ self.fill_work_area() col = self.create_column() return col def duplicate_check(self, name): """ Return True if we have a column in the current entity with same name as our new column :param col_name: column name :type col_name: str """ # check if another column with the same name exist in the current entity if name in self.entity.columns: return True else: return False def rejectAct(self): self.done(0)
class LookupDialog(QDialog, Ui_LookupTranslatorDialog, TranslatorDialogBase): """ Dialog for defining configuration settings for the lookup translation implementation. """ def __init__(self, parent, source_cols, dest_table, dest_col, src_col): QDialog.__init__(self, parent) self.setupUi(self) TranslatorDialogBase.__init__( self, source_cols, dest_table, dest_col, src_col ) self._notif_bar = NotificationBar(self.vl_notification) # Populate controls self._load_lookup_tables() # Connect signals self.cbo_lookup.currentIndexChanged.connect( self._on_lookup_table_name_changed ) def _load_lookup_tables(self): # Load lookup table names c_profile = self._current_profile if c_profile is None: msg = QApplication.translate( 'LookupDialog', 'Current profile could not be determined.' ) self._notif_bar.clear() self._notif_bar.insertErrorNotification(msg) return lookups = [e.name for e in c_profile.value_lists()] self.cbo_lookup.clear() self.cbo_lookup.addItem('') self.cbo_lookup.addItems(lookups) def _on_lookup_table_name_changed(self, idx): # Slot raised when the lookup table name changes. self.cbo_default.clear() if idx == -1: return t_name = self.cbo_lookup.currentText() self.load_lookup_values(t_name) def load_lookup_values(self, table_name): """ Load the default value combobox with values from the specified lookup table name. :param table_name: Lookup table name. :type table_name: str """ self.cbo_default.clear() if not table_name: return # Get lookup entity lk_ent = self._current_profile.entity_by_name(table_name) if lk_ent is None: msg = QApplication.translate( 'LookupDialog', 'Lookup values could not be loaded.' ) self._notif_bar.clear() self._notif_bar.insertErrorNotification(msg) return lk_values = lk_ent.lookups() self.cbo_default.addItem('') for lk_value in lk_values: text_value = None for k, v in lk_ent.values.items(): if v.value == lk_value: text_value = v.value if text_value is not None: self.cbo_default.addItem(text_value) def value_translator(self): """ :return: Returns the lookup value translator object. :rtype: LookupValueTranslator """ lookup_translator = LookupValueTranslator() lookup_translator.set_referencing_table(self._dest_table) lookup_translator.set_referencing_column(self._dest_col) lookup_translator.set_referenced_table(self.cbo_lookup.currentText()) lookup_translator.add_source_reference_column( self._src_col, self._dest_col ) lookup_translator.default_value = self.cbo_default.currentText() return lookup_translator def validate(self): """ Check user configuration and validate if they are correct. :return: Returns True if user configuration is correct, otherwise False. :rtype: bool """ if not self.cbo_lookup.currentText(): msg = QApplication.translate( 'LookupDialog', 'Please select the referenced lookup table.' ) self._notif_bar.clear() self._notif_bar.insertWarningNotification(msg) return False return True def accept(self): """ Validate before accepting user input. """ if self.validate(): super(LookupDialog, self).accept()