diff --git a/.gitignore b/.gitignore index b16cc73d..bbbd3e6f 100755 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ ui.make.bat #sample form extension file stdm/ui/forms/ext/sample_extensions.py + +*.autosave diff --git a/stdm/plugin.py b/stdm/plugin.py index 91187a2e..c8fd1a75 100644 --- a/stdm/plugin.py +++ b/stdm/plugin.py @@ -144,6 +144,8 @@ ) from stdm.utils.util import simple_dialog +from stdm.ui.mobile_data_provider.base_mobile_provider import BaseMobileProvider + from stdm.composer.template_converter import ( TemplateConverterTask ) @@ -207,6 +209,10 @@ def __init__(self, iface): self.configuration_file_updater = ConfigurationFileUpdater(self.iface) copy_startup() + self.mobile_provider_registry = {} + self.providers_menu = [] + self.providers_toolbox = [] + def initGui(self): # Initial actions on starting up the application self._menu_items() @@ -1039,6 +1045,26 @@ def loadModules(self): geoodk_mobile_dataMenu.setIcon(GuiUtils.get_icon("mobile_data_management.png")) geoodk_mobile_dataMenu.setTitle(QApplication.translate("GeoODKMobileSettings", "Mobile Data Forms")) + # Mobile provider menu container + # Mobile provider content menu container + mobile_data_provider_data_menu = QMenu(self.stdmMenu) + mobile_data_provider_data_menu.setObjectName("MobileProviderMenu") + mobile_data_provider_data_menu.setIcon(GuiUtils.get_icon("mobile_data_management.png")) + mobile_data_provider_data_menu.setTitle(QApplication.translate("MobileProviderSettings", "Mobile Data Provider")) + + self.mobile_provider_registry = { + "ODK": {"icon": "mobile_data_management.png", "title": "ODK"}, + "Kobo": {"icon": "mobile_data_management.png", "title": "Kobo"}, + "QFields": {"icon": "mobile_data_management.png", "title": "QFields"} + } + + for k, v in self.mobile_provider_registry.items(): + provider_menu = mobile_data_provider_data_menu.addMenu(k) + provider_menu.setObjectName(k) + provider_menu.setIcon(GuiUtils.get_icon(v["icon"])) + provider_menu.setTitle(v["title"]) + self.providers_menu.append(provider_menu) + geoodkBtn = QToolButton() adminObjName = QApplication.translate("MobileToolbarSettings", "Mobile Data Forms") # Required by module loader for those widgets that need to be inserted into the container @@ -1049,6 +1075,27 @@ def loadModules(self): geoodkMenu = QMenu(geoodkBtn) geoodkBtn.setMenu(geoodkMenu) + + mobile_provider_button = QToolButton() + mobile_provider_object_name = QApplication.translate("MobileToolbarSettings", "Mobile Data Provider") + # Required by module loader for those widgets that need to be inserted into the container + mobile_provider_button.setObjectName(mobile_provider_object_name) + mobile_provider_button.setToolTip(mobile_provider_object_name) + mobile_provider_button.setIcon(GuiUtils.get_icon("mobile_data_management.png")) + mobile_provider_button.setPopupMode(QToolButton.InstantPopup) + + mobile_provider_menu = QMenu(mobile_provider_button) + + for k, v in self.mobile_provider_registry.items(): + provider_toolbox = mobile_provider_menu.addMenu(k) + provider_toolbox.setObjectName(k) + provider_toolbox.setIcon(GuiUtils.get_icon(v["icon"])) + provider_toolbox.setTitle(v["title"]) + self.providers_toolbox.append(provider_toolbox) + + mobile_provider_button.setMenu(mobile_provider_menu) + + # Define actions self.contentAuthAct = QAction( @@ -1124,6 +1171,15 @@ def loadModules(self): QApplication.translate("MobileFormGenerator", "Import Mobile Data"), self.iface.mainWindow()) + self.mobile_provider_export_action = QAction(GuiUtils.get_icon("mobile_collect.png"), + QApplication.translate("MobileProviderExportAction", + "Mobile Provider Export Form"), + self.iface.mainWindow()) + self.mobile_provider_import_action = QAction(GuiUtils.get_icon("mobile_import.png"), + QApplication.translate("MobileProviderImportAction", + "Mobile Provider Import Form"), + self.iface.mainWindow()) + self.details_dock_widget = DetailsDockWidget(map_canvas=self.iface.mapCanvas(), plugin=self) self.details_dock_widget.setToggleVisibilityAction(self.feature_details_act) self.iface.addDockWidget(Qt.RightDockWidgetArea, self.details_dock_widget) @@ -1148,6 +1204,9 @@ def loadModules(self): self.mobile_form_act.triggered.connect(self.mobile_form_generator) self.mobile_form_import.triggered.connect(self.mobile_form_importer) + self.mobile_provider_export_action.triggered.connect(lambda: self.mobile_provider_export_form_generator("ODK")) + self.mobile_provider_import_action.triggered.connect(self.mobile_provider_import_form_generator) + contentMenu.triggered.connect(self.widgetLoader) self.wzdAct.triggered.connect(self.load_config_wizard) self.viewSTRAct.triggered.connect(self.onViewSTR) @@ -1198,6 +1257,12 @@ def loadModules(self): mobileFormImportCnt = ContentGroup.contentItemFromQAction(self.mobile_form_import) mobileFormImportCnt.code = "1394547d-fb6c-4f6e-80d2-53407cf7b7d4" + mobile_provider_form_export_content = ContentGroup.contentItemFromQAction(self.mobile_provider_export_action) + mobile_provider_form_export_content.code = "b3b99492-a895-4513-a456-0fc9083fe11d" + + mobile_provider_form_import_content = ContentGroup.contentItemFromQAction(self.mobile_provider_import_action) + mobile_provider_form_import_content.code = "9eff43f1-3ed5-4db7-8fe3-628b2cc53d01" + username = globals.APP_DBCONN.User.UserName if username == 'postgres': @@ -1322,6 +1387,19 @@ def loadModules(self): geoodkSettingsCntGroup.append(self.mobileXformgenCntGroup) geoodkSettingsCntGroup.append(self.mobileXFormImportCntGroup) + # Mobile provider group + self.mobile_provider_export_content_group = ContentGroup(username, self.mobile_provider_export_action) + self.mobile_provider_export_content_group.addContentItem(mobile_provider_form_export_content) + self.mobile_provider_export_content_group.register() + + self.mobile_provider_import_content_group = ContentGroup(username, self.mobile_provider_import_action) + self.mobile_provider_import_content_group.addContentItem(mobile_provider_form_import_content) + self.mobile_provider_import_content_group.register() + + # Group mobile provider actions to one menu + mobile_provider_action_content_group = [self.mobile_provider_export_content_group, + self.mobile_provider_import_content_group] + # Register document templates # Get templates for the current profile templates = documentTemplates() @@ -1394,6 +1472,15 @@ def loadModules(self): self.menubarLoader.addContents(geoodkSettingsCntGroup, [geoodk_mobile_dataMenu, geoodk_mobile_dataMenu]) self.toolbarLoader.addContents(geoodkSettingsCntGroup, [geoodkMenu, geoodkBtn]) + # Adding mobile provider to toolbar and menu + for p in self.providers_menu: + self.menubarLoader.addContents(mobile_provider_action_content_group, + [p, mobile_data_provider_data_menu]) + + for p in self.providers_toolbox: + self.toolbarLoader.addContents(mobile_provider_action_content_group, + [p, mobile_provider_button]) + self.menubarLoader.addContent(self._action_separator()) self.toolbarLoader.addContent(self._action_separator()) @@ -2197,6 +2284,13 @@ def mobile_form_importer(self): importer_dialog = ProfileInstanceRecords(self.iface.mainWindow()) importer_dialog.exec_() + def mobile_provider_export_form_generator(self, provider): + base_mobile_provider_wizard = BaseMobileProvider(self.iface.mainWindow(), provider) + base_mobile_provider_wizard.exec_() + + def mobile_provider_import_form_generator(self): + pass + def default_config_version(self): handler = self.config_loader() config_version = handler.read_config_version() diff --git a/stdm/ui/mobile_data_provider/__init__.py b/stdm/ui/mobile_data_provider/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/stdm/ui/mobile_data_provider/base_mobile_provider.py b/stdm/ui/mobile_data_provider/base_mobile_provider.py new file mode 100644 index 00000000..d3e880f7 --- /dev/null +++ b/stdm/ui/mobile_data_provider/base_mobile_provider.py @@ -0,0 +1,420 @@ +""" +/*************************************************************************** +Name : wizard +Description : Mobile provider export and import wizard +Date : 25/March/2023 +copyright : (C) 2015 by UN-Habitat and implementing partners. + See the accompanying file CONTRIBUTORS.txt in the root +email : stdm@unhabitat.org + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +from qgis.PyQt import uic +from qgis.PyQt.QtCore import ( + Qt, + QDir, + pyqtSignal, + QDate, + QFile, + QThread, + QCoreApplication, + QEvent, + QIODevice, + QPointF, + QSizeF, + QItemSelectionModel, + QModelIndex, + QSortFilterProxyModel +) +from qgis.PyQt.QtGui import ( + QStandardItemModel, + QStandardItem, + QColor, + QFont +) +from qgis.PyQt.QtWidgets import ( + QWizard, + QMessageBox, + QAbstractItemView, + QDialog, + QApplication, + QMenu, + QFileDialog, + QTableView +) +from qgis._core import QgsMessageLog + +from stdm.data.configuration.db_items import DbItem +from stdm.data.configuration.profile import Profile +from stdm.settings import current_profile +from stdm.ui.gui_utils import GuiUtils +from .column_editor import AppearanceColumnEditor +from .custom_item_model import ( + EntitiesModel, + ColumnEntitiesModel, +) +from stdm.utils.util import enable_drag_sort +from .mobile_provider_export import ProviderWriter +from ...data.pg_utils import pg_table_exists, pg_table_record_count, table_column_names +from ...exceptions import DummyException + +WIDGET, BASE = uic.loadUiType( + GuiUtils.get_ui_file_path('mobile_data_provider/ui_mobile_provider_wizard.ui')) + +HOME = QDir.home().path() +PROVIDER_HOME = HOME + '/.stdm/ui/mobile_data_provider' + + +class BaseMobileProvider(WIDGET, BASE): + """ + STDM mobile provider export and import wizard + """ + wizardFinished = pyqtSignal(object, bool) + + def __init__(self, parent, provider=None): + QWizard.__init__(self, parent) + self.setupUi(self) + + # Add maximize buttons + self.setWindowFlags( + self.windowFlags() | + Qt.WindowSystemMenuHint | + Qt.WindowMaximizeButtonHint + ) + + self.register_fields() + # self.txtMobileProviderIntroduction.setText(self.current_profile().name) + self.entity_model = EntitiesModel() + self.set_views_entity_model(self.entity_model) + self.init_entity_item_model() + self.col_view_model = ColumnEntitiesModel() + self.providerEntityColumns.setModel(self.col_view_model) + self.init_ui_ctrls() + self.providerEntityColumns.installEventFilter(self) + self.load_profiles() + + def register_fields(self): + self.setOption(self.HaveHelpButton, True) + page_count = self.page(1) + + def current_profile(self) -> Profile: + """ + :return:profile object + :rtype: Object + """ + return current_profile() + + def init_entity_item_model(self): + """ + Attach a selection change event handler for the custom + QStandardItemModel - entity_item_model + """ + self.entity_item_model = self.providerEntities.selectionModel() + self.entity_item_model.selectionChanged.connect(self.entity_changed) + + def set_views_entity_model(self, entity_model): + """ + Attach custom item model to hold data for the view controls + :param entity_model: Custom QStardardItemModel + :type entity_model: EntitiesModel + """ + self.providerEntities.setModel(entity_model) + + def init_ui_ctrls(self): + """ + Set default state for UI controls + """ + self.providerEntityColumns.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.providerEntityColumns.doubleClicked.connect(self.edit_column) + + self.columns_proxy_model = QSortFilterProxyModel(self.col_view_model) + self.columns_proxy_model.setSourceModel(self.col_view_model) + self.columns_proxy_model.setFilterKeyColumn(0) + self.providerEntityColumns.setModel(self.columns_proxy_model) + self.providerEntityColumns.setSortingEnabled(True) + self.providerEntities.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.providerEntities.clicked.connect(self.entity_clicked) + enable_drag_sort(self.providerEntityColumns) + + def size_columns_viewer(self): + NAME = 0 + TYPE = 1 + DESC = 2 + + self.providerEntityColumns.setColumnWidth(NAME, 200) + self.providerEntityColumns.setColumnWidth(TYPE, 180) + self.providerEntityColumns.setColumnWidth(DESC, 200) + + def edit_column(self): + """ + Event handler for editing a column. + """ + if len(self.providerEntityColumns.selectedIndexes()) == 0: + self.show_message(self.tr("Please select a column to edit")) + return + + rid, column, model_item = self.get_selected_item_data(self.providerEntityColumns) + if rid == -1: + return + if column and column.action == DbItem.CREATE: + _, entity = self._get_entity(self.providerEntities) + + profile = self.current_profile() + params = {} + params['parent'] = self + params['column'] = column + params['entity'] = entity + params['profile'] = profile + params['in_db'] = self.column_exist_in_entity(entity, column) + params['entity_has_records'] = self.entity_has_records(entity) + + params['is_new'] = False + + original_column = column # model_item.entity(column.name) + + editor = AppearanceColumnEditor(**params) + result = editor.exec_() + + if result == 1: + + model_index_name = model_item.index(rid, 0) + model_index_dtype = model_item.index(rid, 1) + model_index_desc = model_item.index(rid, 2) + + model_item.setData(model_index_name, editor.column.name) + model_item.setData(model_index_dtype, editor.column.display_name()) + model_item.setData(model_index_desc, editor.column.description) + + model_item.edit_entity(original_column, editor.column) + + entity.columns[original_column.name] = editor.column + entity.rename_column(original_column.name, editor.column.name) + + else: + self.show_message(QApplication.translate("Configuration Wizard", \ + "No column selected for edit!")) + + def entity_changed(self): + """ + triggered when you select an entity, clears an existing column entity + model and create a new one. + Get the columns of the selected entity, add them to the newly created + column entity model + """ + self.trigger_entity_change() + + def trigger_entity_change(self): + row_id = self.entity_item_model.currentIndex().row() + view_model = self.entity_item_model.currentIndex().model() + self.col_view_model.clear() + self.col_view_model = ColumnEntitiesModel() + + if row_id > -1: + # columns = view_model.entity_byId(row_id).columns.values() + ent_name = view_model.data(view_model.index(row_id, 0)) + + entity = view_model.entity(ent_name) + if entity is None: + return + columns = list(view_model.entity(ent_name).columns.values()) + self.add_columns(self.col_view_model, columns) + + self.providerEntityColumns.setModel(self.col_view_model) + self.size_columns_viewer() + + def add_columns(self, v_model, columns): + """ + Add columns to a view model + param v_model: Instance of EntitiesModel + type v_model: EntitiesModel + param columns: List of column names to insert in v_model + type columns: list + """ + for column in columns: + if column.user_editable(): + v_model.add_entity(column) + + def entity_clicked(self): + _, entity, _ = self.get_selected_item_data(self.providerEntities) + if entity is None: + return + profile = current_profile() + if profile is None: + return + + def get_selected_item_data(self, view): + if len(view.selectedIndexes()) == 0: + return -1, None, None + model_item = view.model() + row_id = view.selectedIndexes()[0].row() + col_name = view.model().data( + view.model().index(row_id, 0)) + column = model_item.entities()[str(col_name)] + return row_id, column, model_item + + def load_profiles(self): + """ + Read and load profiles from StdmConfiguration instance + """ + self.populate_view_models(current_profile()) + + 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_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 _get_entity(self, view): + model_item, entity, row_id = self.get_model_entity(view) + if entity: + return row_id, entity + + def get_model_entity(self, view): + """ + Extracts and returns an entitymodel, entity and the + current selected item ID from QAbstractItemView of a + given view widget + param view: Widget that inherits QAbstractItemView + type view: QAbstractitemView + rtype: tuple - (QStandardItemModel, Entity, int) + """ + sel_id = -1 + entity = None + model_indexes = view.selectedIndexes() + if len(model_indexes) == 0: + return (None, None, None) + model_index = model_indexes[0] + model_item = model_index.model() + name = model_item.data(model_index) + entity = model_item.entity(name) + row = model_index.row() + + return (model_item, entity, row) + + def entity_has_records(self, entity): + """ + Returns True if entity has records in the database else False. + :param entity: Entity instance + :type entity: Entity + :rtype: boolean + """ + if not pg_table_exists(entity.name): + return False + + record_count = pg_table_record_count(entity.name) + return True if record_count > 0 else False + + def column_exist_in_entity(self, entity, column): + """ + Returns True if a column exists in a given entity in DB + :param entity: Entity instance to check if column exist + :type entity: Entity + :param column: Column instance to check its existance + :type entity: BaseColumn + :rtype: boolean + """ + cols = table_column_names(entity.name) + return True if column.name in cols else False + + def validateCurrentPage(self): + validPage = True + + if self.nextId() == 1: + return validPage + + if self.currentId() == 1: + QgsMessageLog.logMessage("Page 1", "STDM dev") + validPage = True + + elif self.currentId() == 2: + QgsMessageLog.logMessage("Page 2", "STDM dev") + + entities = self.selected_entities_from_model() + QgsMessageLog.logMessage("Entities {entities}".format(entities=str(entities)), "STDM dev") + if len(entities) == 0: + QgsMessageLog.logMessage("No entity selected. Please select at least one entity...", "STDM dev") + self._notif_bar_str.insertErrorNotification( + 'No entity selected. Please select at least one entity...' + ) + return + + else: + QgsMessageLog.logMessage("Created", "STDM dev") + self.generate_mobile_form(entities) + + validPage = False + + return validPage + + def generate_provider_output(self, profile): + pass + + def selected_entities_from_model(self) ->list[str]: + """ + Get selected entities for conversion to Xform. + """ + entity_list = [] + if self.entity_model.rowCount() > 0: + for row in range(self.entity_model.rowCount()): + item = self.entity_model.item(row) + entity_list.append(item.text()) + return entity_list + + def generate_mobile_form(self, profile_entities: list[str]): + """ + Generate mobile form for entities of the current profile. + """ + try: + + profile = self.current_profile() + + provider_writer = ProviderWriter(profile, profile_entities) + + created, create_msg = provider_writer.create_xsl_file() + if not created: + QgsMessageLog.logMessage("Message {msg}".format(msg=create_msg), "STDM dev") + return + + data_written, write_msg = provider_writer.write_data_to_xlsform() + if not data_written: + QgsMessageLog.logMessage("Message {msg}".format(msg=write_msg), "STDM dev") + return + else: + QgsMessageLog.logMessage("Message {msg}".format(msg=write_msg), "STDM dev") + + except DummyException as ex: + QgsMessageLog.logMessage("Message {msg}".format(msg=ex.message + + ': Unable to generate Mobile Form'), "STDM dev") + diff --git a/stdm/ui/mobile_data_provider/column_editor.py b/stdm/ui/mobile_data_provider/column_editor.py new file mode 100644 index 00000000..d0c308d8 --- /dev/null +++ b/stdm/ui/mobile_data_provider/column_editor.py @@ -0,0 +1,337 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** +Name : column_editor +Description : Editor to create/edit entity columns +Date : 24/January/2016 +copyright : (C) 2015 by UN-Habitat and implementing partners. + See the accompanying file CONTRIBUTORS.txt in the root +email : stdm@unhabitat.org + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +import datetime +import logging + +from qgis.PyQt import uic +from qgis.PyQt.QtCore import ( + Qt, + QRegExp, + QSettings, + QEvent, +) +from qgis.PyQt.QtGui import ( + QRegExpValidator, + QValidator +) +from qgis.PyQt.QtWidgets import ( + QDialog, + QApplication, + QDialogButtonBox, + QMessageBox +) + +from stdm.data.configuration.columns import BaseColumn +from stdm.ui.gui_utils import GuiUtils +from stdm.ui.notification import NotificationBar + +WIDGET, BASE = uic.loadUiType( + GuiUtils.get_ui_file_path('mobile_data_provider/ui_mobile_provider_column_editor.ui')) + +LOGGER = logging.getLogger('stdm') +LOGGER.setLevel(logging.DEBUG) + +RESERVED_KEYWORDS = [ + 'id', 'documents', 'spatial_unit', 'supporting_document', + 'social_tenure', 'social_tenure_relationship', 'geometry', + 'social_tenure_relationship_id' +] + + +class AppearanceColumnEditor(WIDGET, BASE): + """ + 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) + self.entity_has_records = kwargs.get('entity_has_records', False) + + QDialog.__init__(self, self.form_parent) + + self.FK_EXCLUDE = ['supporting_document', '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() + + self.appearanceColumnDataType.currentIndexChanged.connect(self.change_data_type) + + self.notice_bar = NotificationBar(self.notif_bar) + self._exclude_col_type_info = [] + self.init_controls() + + 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} + } + + 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 + } + + 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} + + 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 + } + + 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} + + 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} + } + + 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} + } + + 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} + } + + 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} + } + + 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, + '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} + } + + def init_controls(self): + """ + Initialize GUI controls default state when the dialog window is opened. + """ + self.populate_data_type_cbo() + + self.buttonBox.button(QDialogButtonBox.Ok).clicked.connect(self.accept) + self.buttonBox.button(QDialogButtonBox.Cancel).clicked.connect(self.cancel) + + def populate_data_type_cbo(self): + """ + Fills the data type combobox widget with BaseColumn type names. + """ + self.appearanceColumnDataType.clear() + + appearance_dictionary = { + 'text': ['none', 'numbers', 'ex.*', 'url'], + 'integer': ['none', 'thousands-sep'], + 'decimal': ['none', 'thousands-sep'], + 'date': ['none', 'no-calendar', 'month-year', 'year', 'coptic', 'ethiopian', 'islamic', 'bikram-sambat', + 'myanmar', 'persian'], + 'select_one': ['minimal', 'quick', 'autocomplete', 'columns-pack', 'columns'], + 'select_multiple': ['none'], + 'select_one_from_file': ['none'], + 'geopoint': ['none', 'maps', 'placement-map'], + 'geotrace': ['none'], + 'geoshape': ['none'] + } + + col_type = self._column_type_info(self.column) + + if col_type == 'VARCHAR': + for appearance in appearance_dictionary['text']: + self.appearanceColumnDataType.addItem(appearance) + + elif col_type == 'INT': + for appearance in appearance_dictionary['integer']: + self.appearanceColumnDataType.addItem(appearance) + + elif col_type == 'DOUBLE': + for appearance in appearance_dictionary['decimal']: + self.appearanceColumnDataType.addItem(appearance) + + elif col_type == 'DATE': + for appearance in appearance_dictionary['date']: + self.appearanceColumnDataType.addItem(appearance) + + elif col_type == 'LOOKUP': + for appearance in appearance_dictionary['select_one']: + self.appearanceColumnDataType.addItem(appearance) + + elif col_type == 'MULTIPLE_SELECT': + for appearance in appearance_dictionary['select_multiple']: + self.appearanceColumnDataType.addItem(appearance) + + elif col_type == 'GEOMETRY': + for appearance in appearance_dictionary['geopoint']: + self.appearanceColumnDataType.addItem(appearance) + + if self.appearanceColumnDataType.count() > 0: + self.appearanceColumnDataType.setCurrentIndex(0) + + def change_data_type(self, index): + """ + Called by type combobox when you select a different data type. + """ + text = self.appearanceColumnDataType.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.type_info = ti + opts = self.type_attribs[ti] + self.set_optionals(opts) + + 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 + """ + pass + + 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 cancel(self): + self.done(0) + + def accept(self): + self.done(1) diff --git a/stdm/ui/mobile_data_provider/custom_item_model.py b/stdm/ui/mobile_data_provider/custom_item_model.py new file mode 100644 index 00000000..aec42aab --- /dev/null +++ b/stdm/ui/mobile_data_provider/custom_item_model.py @@ -0,0 +1,543 @@ +import logging +from collections import OrderedDict + +from qgis.PyQt.QtCore import ( + Qt +) +from qgis.PyQt.QtGui import ( + QStandardItem, + QStandardItemModel, + QBrush, + QColor, + QIcon +) +from qgis.PyQt.QtWidgets import ( + QTableView, + QAbstractItemView, + QComboBox, + QListView +) +from qgis._core import QgsMessageLog + +from stdm.ui.gui_utils import GuiUtils + +LOGGER = logging.getLogger('stdm') + + +class EntityModelItem(QStandardItem): + def __init__(self, entity_name): + self._entity = None + + super(EntityModelItem, self).__init__(entity_name) + + #self.setColumnCount(len(self.headers_labels)) + + # if not entity is None: + # self.set_entity(entity) + + # def entity(self): + # return self._entity + + # def _create_item(self, text): + # item = QStandardItem(text) + + # return item + + # def _set_entity_properties(self): + # name_item = self._create_item(self._entity.short_name) + # description = self._create_item(str(self._entity.description)) + + # self.appendRow([name_item, description]) + + # def set_entity(self, entity): + # self._entity = entity + # self._set_entity_properties() + + +class EntitiesModel(QStandardItemModel): + headers_labels = ["Name", "Has Supporting Document?", "Description"] + + def __init__(self, parent=None): + super(EntitiesModel, self).__init__(parent) + + self._entities = OrderedDict() + + self.setHorizontalHeaderLabels(EntitiesModel.headers_labels) + + def supportedDragActions(self): + return Qt.MoveAction + + def entity(self, name): + if name in self._entities: + return self._entities[name] + + return None + + def entity_byId(self, id): + return list(self._entities.values())[id] + + def entities(self): + return self._entities + + def add_entity(self, entity): + if not entity.short_name in self._entities: + self._add_row(entity) + self._entities[entity.short_name] = entity + + def edit_entity(self, old_entity_name, new_entity): + self._entities[old_entity_name] = new_entity + self._entities[new_entity.short_name] = \ + self._entities.pop(old_entity_name) + + # ++ + def delete_entity(self, entity): + if entity.short_name in self._entities: + name = entity.short_name + del self._entities[name] + LOGGER.debug('%s model entity removed.', name) + + def delete_entity_byname(self, short_name): + if short_name in self._entities: + del self._entities[short_name] + LOGGER.debug('%s model entity removed.', short_name) + + def _add_row(self, entity): + ''' + name_item = QStandardItem(entity.name()) + mandatory_item = QStandardItem(str(entity.mandatory())) + self.appendRow([name_item, mandatory_item]) + ''' + # entity_item = EntityModelItem(entity) + name_item = EntityModelItem(entity.short_name) + name_item.setData(GuiUtils.get_icon_pixmap("table02.png"), Qt.DecorationRole) + + support_doc = EntityModelItem(self.bool_to_yesno(entity.supports_documents)) + support_doc.setTextAlignment(Qt.AlignHCenter|Qt.AlignVCenter) + description = EntityModelItem(entity.description) + + self.appendRow([name_item, support_doc, description]) + + def bool_to_yesno(self, state: bool) -> str: + CHECK_STATE = {True: 'Yes', False: 'No'} + return CHECK_STATE[state] + + +class BaseEntitySelectionMixin(object): + def selected_entities(self): + model = self.model() + if not isinstance(model, EntitiesModel): + raise TypeError('Model is not of type ') + + selected_names = self._selected_names(model) + + entities = model.entities() + + return [entities[name] for name in selected_names if name in entities] + + def _names_from_indexes(self, model, selected_indexes): + return [str(model.itemFromIndex(idx).text()) for idx in selected_indexes if idx.isValid()] + + def _selected_names(self, model): + raise NotImplementedError('Please use the sub-class object of ') + + +class EntityTableSelectionMixin(BaseEntitySelectionMixin): + def _selected_names(self, model): + sel_indexes = self.selectionModel().selectedRows() + + return self._names_from_indexes(model, sel_indexes) + + +class EntityListSelectionMixin(BaseEntitySelectionMixin): + def _selected_names(self, model): + sel_indexes = self.selectionModel().selectedIndexes() + + return self._names_from_indexes(model, sel_indexes) + + +class EntityComboBoxSelectionMixin(BaseEntitySelectionMixin): + def _selected_names(self, model): + return [str(self.currentText())] + + def current_entity(self): + entities = self.selected_entities() + + if len(entities) > 0: + return entities[0] + + return None + + +class EntitiesTableView(QTableView, EntityTableSelectionMixin): + def __init__(self, parent=None): + super(EntitiesTableView, self).__init__(parent) + self.setSelectionBehavior(QAbstractItemView.SelectRows) + self.setSelectionMode(QAbstractItemView.ContiguousSelection) + self.setEditTriggers(QAbstractItemView.NoEditTriggers) + + +class EntitiesListView(QListView, EntityListSelectionMixin): + def __init__(self, parent=None): + super(EntitiesListView, self).__init__(parent) + self.setSelectionMode(QAbstractItemView.SingleSelection) + self.setEditTriggers(QAbstractItemView.NoEditTriggers) + + +class EntitiesComboView(QComboBox, EntityComboBoxSelectionMixin): + def __init__(self, parent=None): + super(EntitiesComboView, self).__init__(parent) + + +#### +# Column Entity model item +############ + +class ColumnEntityModelItem(QStandardItem): + def __init__(self, column_name: str =""): + self._column_name = column_name + super(ColumnEntityModelItem, self).__init__(column_name) + + +class ColumnEntitiesModel(QStandardItemModel): + # headers_labels = ["Name", "Data Type", "Mandatory", "Unique", "Description"] + headers_labels = ["Name", "STDM Data Type", "Description", "ODK Data Type", "Appearance"] + + def __init__(self, parent=None): + super(ColumnEntitiesModel, self).__init__(parent) + self._entities = OrderedDict() + self.odk_data_type_and_appearance = { + 'Varying-length Text': ['text', 'none'], + 'Single Select Lookup': ['select_one', 'none'], + 'Date': ['date', 'none'], + 'Multiple Select Lookup': ['select_multiple', 'none'], + 'Geometry': ['geoshape', 'none'], + 'Administrative Spatial Unit': ['select_one', 'none'], + 'Auto Generated Code': ['integer', 'none'], + 'Whole Number': ['integer', 'none'], + 'Decimal Number': ['integer', 'none'] + } + + self.setHorizontalHeaderLabels(ColumnEntitiesModel.headers_labels) + + def _add_row(self, entity): + name_column = ColumnEntityModelItem(entity.name) + + data_type_name = entity.display_name() + odk_data_type_name = '' + odk_data_type_appearance = '' + + QgsMessageLog.logMessage('data type {data_type_name} type info {type_info} ' + .format(data_type_name=data_type_name, type_info=entity.TYPE_INFO), 'STDM dev') + + if entity.TYPE_INFO == 'VARCHAR': + data_type_name_info = data_type_name + data_type_name = f'{data_type_name} ({entity.maximum})' + odk_data_type_name = self.odk_data_type_and_appearance[data_type_name_info][0] + odk_data_type_appearance = self.odk_data_type_and_appearance[data_type_name_info][1] + + elif entity.TYPE_INFO == 'INT': + data_type_name_info = data_type_name + data_type_name = data_type_name + odk_data_type_name = self.odk_data_type_and_appearance[data_type_name_info][0] + odk_data_type_appearance = self.odk_data_type_and_appearance[data_type_name_info][1] + + elif entity.TYPE_INFO == 'DOUBLE': + data_type_name_info = data_type_name + data_type_name = data_type_name + odk_data_type_name = self.odk_data_type_and_appearance[data_type_name_info][0] + odk_data_type_appearance = self.odk_data_type_and_appearance[data_type_name_info][1] + + elif entity.TYPE_INFO == 'LOOKUP': + data_type_name_info = data_type_name + data_type_name = f'{data_type_name} ({entity.maximum})' + odk_data_type_name = self.odk_data_type_and_appearance[data_type_name_info][0] + odk_data_type_appearance = self.odk_data_type_and_appearance[data_type_name_info][1] + + elif entity.TYPE_INFO == 'DATE': + data_type_name_info = data_type_name + data_type_name = f'{data_type_name} ({entity.maximum})' + odk_data_type_name = self.odk_data_type_and_appearance[data_type_name_info][0] + odk_data_type_appearance = self.odk_data_type_and_appearance[data_type_name_info][1] + + elif entity.TYPE_INFO == 'MULTIPLE_SELECT': + data_type_name_info = data_type_name + data_type_name = data_type_name + odk_data_type_name = self.odk_data_type_and_appearance[data_type_name_info][0] + odk_data_type_appearance = self.odk_data_type_and_appearance[data_type_name_info][1] + + elif entity.TYPE_INFO == 'GEOMETRY': + data_type_name_info = data_type_name + data_type_name = data_type_name + odk_data_type_name = self.odk_data_type_and_appearance[data_type_name_info][0] + odk_data_type_appearance = self.odk_data_type_and_appearance[data_type_name_info][1] + + elif entity.TYPE_INFO == 'ADMIN_SPATIAL_UNIT': + data_type_name_info = data_type_name + data_type_name = data_type_name + odk_data_type_name = self.odk_data_type_and_appearance[data_type_name_info][0] + odk_data_type_appearance = self.odk_data_type_and_appearance[data_type_name_info][1] + + elif entity.TYPE_INFO == 'AUTO_GENERATED': + data_type_name_info = data_type_name + data_type_name = data_type_name + odk_data_type_name = self.odk_data_type_and_appearance[data_type_name_info][0] + odk_data_type_appearance = self.odk_data_type_and_appearance[data_type_name_info][1] + + data_type_column = ColumnEntityModelItem(data_type_name) + + # mandt_column = ColumnEntityModelItem( + # self.bool_to_yesno(entity.mandatory)) + # mandt_column.setTextAlignment(Qt.AlignHCenter| Qt.AlignVCenter) + # if entity.mandatory: + # brush = QBrush(Qt.red) + # mandt_column.setForeground(brush) + + # unique_column = ColumnEntityModelItem( + # self.bool_to_yesno(entity.unique) + # ) + # unique_column.setTextAlignment(Qt.AlignHCenter|Qt.AlignVCenter) + + description_column = ColumnEntityModelItem(entity.description) + odk_data_type_name_column = ColumnEntityModelItem(odk_data_type_name) + odk_data_type_appearance_column = ColumnEntityModelItem(odk_data_type_appearance) + # self.appendRow([name_column, data_type_column, + # mandt_column, unique_column, description_column]) + + self.appendRow([ + name_column, + data_type_column, + description_column, + odk_data_type_name_column, + odk_data_type_appearance_column] + ) + + def bool_to_yesno(self, state: bool) -> str: + CHECK_STATE = {True: 'Yes', False: 'No'} + return CHECK_STATE[state] + + def supportedDragActions(self): + return Qt.MoveAction + + def entity(self, name): + if name in self._entities: + return self._entities[name] + return None + + def entity_byId(self, id): + return list(self._entities.values())[id] + + def entities(self): + return self._entities + + def add_entity(self, entity): + if not entity.name in self._entities: + self._add_row(entity) + self._entities[entity.name] = entity + + def edit_entity(self, old_entity, new_entity): + self._entities[old_entity.name] = new_entity + old_key = old_entity.name + new_key = new_entity.name + self._replace_key(old_key, new_key) + + # ++ + def delete_entity(self, entity): + if entity.name in self._entities: + name = entity.name + del self._entities[name] + LOGGER.debug('%s model entity removed.', name) + + def delete_entity_byname(self, short_name): + if short_name in self._entities: + del self._entities[short_name] + LOGGER.debug('%s model entity removed.', short_name) + self._refresh_entities() + + def _replace_key(self, old_key, new_key): + """ + Relace a dictionary key + :param old_key: name of the key to replace + :type old_key: str + :param new_key: name of new key + :type new_key: str + """ + self._entities = OrderedDict([(new_key, v) + if k == old_key else (k, v) for k, v in self._entities.items()]) + + +###### +# Lookup Entity item model +######### + +class LookupEntityModelItem(QStandardItem): + def __init__(self, entity=None): + self._entity = None + + super(LookupEntityModelItem, self).__init__(entity.short_name) + + #self.setColumnCount(len(self.headers_labels)) + + if not entity is None: + self.set_entity(entity) + + def entity(self): + return self._entity + + def _create_item(self, text): + item = QStandardItem(text) + + return item + + def _set_entity_properties(self): + item_name = self._create_item(self._entity.short_name) + + self.appendRow([item_name]) + + def set_entity(self, entity): + self._entity = entity + self._set_entity_properties() + + def set_default_bg_color(self): + brush = QBrush(Qt.black) + self.setForeground(brush) + + def indicate_as_empty(self): + brush = QBrush(Qt.red) + self.setForeground(brush) + + +class LookupEntitiesModel(QStandardItemModel): + headers_labels = ["Name"] + def __init__(self, parent=None): + super(LookupEntitiesModel, self).__init__(parent) + self._entities = OrderedDict() + + self.setHorizontalHeaderLabels(LookupEntitiesModel.headers_labels) + + def entity(self, name): + if name in self._entities: + return self._entities[name] + + return None + + def entity_byId(self, id): + return list(self._entities.values())[id] + + def entities(self): + return self._entities + + def add_entity(self, entity): + if not entity.short_name in self._entities: + self._add_row(entity) + self._entities[entity.short_name] = entity + + def edit_entity(self, old_entity_name, new_entity): + self._entities[old_entity_name] = new_entity + self._entities[new_entity.short_name] = \ + self._entities.pop(old_entity_name) + + # ++ + def delete_entity(self, entity): + if entity.short_name in self._entities: + name = entity.short_name + del self._entities[name] + LOGGER.debug('%s model entity removed.', name) + + def delete_entity_byname(self, short_name): + if short_name in self._entities: + del self._entities[short_name] + LOGGER.debug('%s model entity removed.', short_name) + + def _add_row(self, entity): + entity_item = LookupEntityModelItem(entity) + entity_item.setData(GuiUtils.get_icon_pixmap("bullets_sm.png"), Qt.DecorationRole) + if entity.is_empty(): + brush = QBrush(Qt.red) + entity_item.setForeground(brush) + self.appendRow(entity_item) + + def model_item(self, row: int) -> LookupEntityModelItem: + return self.item(row) + + +################ +# Social Tenure Relationship model item +class STREntityModelItem(QStandardItem): + headers_labels = ["Name", "Description"] + + def __init__(self, entity=None): + self._entity = None + self.name = "" + + super(STREntityModelItem, self).__init__(entity.name) + + self.setColumnCount(len(self.headers_labels)) + self.setCheckable(True) + + if not entity is None: + self.set_entity(entity) + + def entity(self): + return self._entity + + def _create_item(self, text): + item = QStandardItem(text) + + return item + + def _set_entity_properties(self): + name_item = self._create_item(self._entity.name) + self.name = name_item + description = self._create_item(str(self._entity.description)) + + self.appendRow([name_item, description]) + + def set_entity(self, entity): + self._entity = entity + self._set_entity_properties() + + +class STRColumnEntitiesModel(QStandardItemModel): + def __init__(self, parent=None): + super(STRColumnEntitiesModel, self).__init__(parent) + self._entities = OrderedDict() + + self.setHorizontalHeaderLabels(STREntityModelItem.headers_labels) + + def entity(self, name): + if name in self._entities: + return self._entities[name] + + return None + + def entity_byId(self, id): + return list(self._entities.values())[id] + + def entities(self): + return self._entities + + def add_entity(self, entity): + if not entity.name in self._entities: + self._add_row(entity) + self._entities[entity.name] = entity + + # ++ + def delete_entity(self, entity): + if entity.name in self._entities: + name = entity.name + del self._entities[name] + LOGGER.debug('%s model entity removed.', name) + + def delete_entity_byname(self, short_name): + if short_name in self._entities: + del self._entities[short_name] + LOGGER.debug('%s model entity removed.', short_name) + + def _add_row(self, entity): + ''' + name_item = QStandardItem(entity.name()) + mandatory_item = QStandardItem(str(entity.mandatory())) + self.appendRow([name_item, mandatory_item]) + ''' + entity_item = STREntityModelItem(entity) + self.appendRow(entity_item) diff --git a/stdm/ui/mobile_data_provider/mobile_provider_entity_reader.py b/stdm/ui/mobile_data_provider/mobile_provider_entity_reader.py new file mode 100644 index 00000000..266abc97 --- /dev/null +++ b/stdm/ui/mobile_data_provider/mobile_provider_entity_reader.py @@ -0,0 +1,104 @@ +from collections import OrderedDict + +from qgis._core import QgsMessageLog + +from stdm.settings import current_profile + + +class EntityReader: + """ + Class to read entity info from profile and return the entity data + """ + + def __init__(self, entity: str): + + self.entity_name = None + self.user_entity = entity + + def profile(self): + """ + :return:profile object + :rtype: Object + """ + return current_profile() + + def entity_columns(self): + """ + :param entity: + """ + cols = current_profile().entity_by_name(self.entity_name).columns + return cols + + def read_attributes(self): + """ + Test implementation hand codes the entity but would need to be + implemented on the dialog for user to make their own selection + :param self: + :return:dict of entity column and data type + if column.display_name() == 'Related Entity' or column.name == 'id': + """ + self.set_user_selected_entity() + self.entity_attributes = OrderedDict() + columns = list(self.entity_columns().values()) + for column in columns: + # Don't include related entity columns and id columns + if column.display_name() == 'Related Entity' or column.name == 'id': + continue + self.entity_attributes[column.name] = column.TYPE_INFO + return self.entity_attributes + + def set_user_selected_entity(self): + """ + Get user selected list of entities + :param user_sel: string, list + :return: string + """ + self.entity_name = self.profile().entity(self.user_entity).name + + def on_column_info(self, item_col): + """ + get lookup associated with the column + :param item_col: + :return:bool + """ + is_lookup = False + lk_val = self.entity_attributes.get(item_col) + if lk_val == "MULTIPLE_SELECT" or lk_val == "LOOKUP" or lk_val == "BOOL": + is_lookup = True + return is_lookup + + def default_entity(self): + """ + Use to select an entity that will be treated as default + :return: str + """ + return self.entity_name + + # def column_lookup_mapping(self): + # """ + # Check if the column has a lookup and get the associated lookup definition + # :return: + # """ + # lk_attributes = OrderedDict() + # col_objs = list(self.entity_columns().values()) + # for col in col_objs: + # if col.TYPE_INFO == "LOOKUP" or col.TYPE_INFO == "MULTIPLE_SELECT" or col.TYPE_INFO == "BOOL": + # value_list = col.value_list + # lk_attributes[col.name] = str(value_list.name) + # return lk_attributes + + def format_lookup_items(self, col): + """ + Get column lookup for the given lookup column type + :param col: + :return: + """ + col_attributes = OrderedDict() + if self.on_column_info(col): + col_obj = self.entity_columns().get(col) + value_list = col_obj.value_list + for val in value_list.values.values(): + col_attributes[str(val.value)] = str(val.code) + return col_attributes + else: + return None \ No newline at end of file diff --git a/stdm/ui/mobile_data_provider/mobile_provider_export.py b/stdm/ui/mobile_data_provider/mobile_provider_export.py new file mode 100644 index 00000000..b340964c --- /dev/null +++ b/stdm/ui/mobile_data_provider/mobile_provider_export.py @@ -0,0 +1,446 @@ +import os +from openpyxl import Workbook, load_workbook + +from pathlib import Path +from PyQt5.QtCore import QDir, QFile, QIODevice +from qgis._core import QgsMessageLog +from collections import OrderedDict + +from stdm.ui.mobile_data_provider.mobile_provider_entity_reader import EntityReader + +HOME = QDir.home().path() +DOCEXTENSION = '.xlsx' +STDM_FOLDER_PATH = '/.stdm' +PROVIDER_HOME = HOME + STDM_FOLDER_PATH + '/provider_forms/forms' + + +class XLSFormWriter: + + def __init__(self, file_name): + self.file_name = file_name+DOCEXTENSION + self.form = None + + def create_xls_form(self): + + Path(PROVIDER_HOME).mkdir(parents=True, exist_ok=True) + home_directory = os.path.expanduser('~') + self.form = os.path.join(home_directory, '.stdm', 'provider_forms', 'forms', self.file_name) + failed = False + sheet = None + QgsMessageLog.logMessage("Form file path {path}".format(path=self.form), "STDM dev") + + # self.form = QFile(os.path.join(PROVIDER_HOME, self.file_name)) + + # if not self.form.open(QIODevice.ReadWrite | QIODevice.Truncate | + # QIODevice.Text): + # error = self.form.error() + # failed = False + # reason = "" + # + # if error == QFile.OpenError: + # reason = "The file could not be opened." + # + # if error == QFile.AbortError: + # reason = "The operation was aborted." + # + # if error == QFile.TimeOutError: + # reason = "A timeout occurred." + # + # if error == QFile.UnspecifiedError: + # reason = "An unspecified error occurred." + # + # self.form = None + # return failed, reason + # + # else: + # # Load an existing workbook + # work_book = open(r'C:\Users\Lenovo\.stdm\provider_forms\forms\Informal_Settlement.xlsx', 'rb') + # try: + # # Open or create the Excel file + # if os.path.exists(self.form): + # workbook = openpyxl.load_workbook(self.form) + # else: + # workbook = openpyxl.Workbook() + # + # # Check if the worksheet already exists + # if 'survey' in workbook.sheetnames: + # worksheet1 = workbook['survey'] + # else: + # # Create a new worksheet with name "MySheet" + # worksheet1 = workbook.create_sheet("survey") + # + # # Add column headers to the new worksheet + # if 'type' not in [cell.value for cell in worksheet1[1]]: + # worksheet1['A'] = 'type' + # if 'name' not in [cell.value for cell in worksheet1[1]]: + # worksheet1['B'] = 'name' + # if 'label' not in [cell.value for cell in worksheet1[1]]: + # worksheet1['C'] = 'label' + # + # if 'choices' in workbook.sheetnames: + # worksheet2 = workbook['choices'] + # else: + # # Create a new worksheet with name "MySheet" + # worksheet2 = workbook.create_sheet("choices") + # + # # Add column headers to the new worksheet + # if 'list_name' not in [cell.value for cell in worksheet2[2]]: + # worksheet2['A'] = 'list_name' + # if 'name' not in [cell.value for cell in worksheet2[2]]: + # worksheet2['B'] = 'name' + # if 'label' not in [cell.value for cell in worksheet2[2]]: + # worksheet2['C'] = 'label' + # + # if 'settings' in workbook.sheetnames: + # worksheet3 = workbook['settings'] + # else: + # # Create a new worksheet with name "MySheet" + # worksheet3 = workbook.create_sheet("settings") + # + # # Add column headers to the new worksheet + # if 'form_title' not in [cell.value for cell in worksheet3[3]]: + # worksheet3['A'] = 'form_title' + # if 'form_id' not in [cell.value for cell in worksheet3[3]]: + # worksheet3['B'] = 'form_id' + # if 'version' not in [cell.value for cell in worksheet3[3]]: + # worksheet3['C'] = 'version' + # if 'instance_name' not in [cell.value for cell in worksheet3[3]]: + # worksheet3['D'] = 'instance_name' + # + # # Save the workbook to a file + # workbook.save(self.form) + # + # return True, "" + # + # except Exception as e: + # reason = "Error: {error}".format(error=str(e)) + # return failed, reason + sheets = {'survey': ['type', 'name', 'label'], + 'choices': ['list_name', 'name', 'label'], + 'settings': ['form_title', 'form_id', 'version', 'instance_name']} + + # try: + # # check if file exists + # if os.path.isfile(self.form): + # # file exists, load workbook + # workbook = load_workbook(self.form) + # # delete default first sheet if it exists + # if 'Sheet' in workbook.sheetnames: + # sheet = workbook['Sheet'] + # workbook.remove(sheet) + # QgsMessageLog.logMessage("Default sheet 'Sheet' removed from the workbook.", "STDM dev") + # for sheetname, headers in sheets.items(): + # # check if sheet exists + # if sheetname in workbook.sheetnames: + # sheet = workbook[sheetname] + # QgsMessageLog.logMessage(f"{sheetname} already exists in {self.form}.", "STDM dev") + # else: + # # create new sheet + # sheet = workbook.create_sheet(sheetname) + # # add column headers + # sheet.append(headers) + # # save the workbook + # workbook.save(self.form) + # QgsMessageLog.logMessage(f"{sheetname} created in {self.form} with column headers: " + # f"{', '.join(headers)}.", "STDM dev") + # # check if headers exist + # existing_headers = [cell.value for cell in sheet[1]] + # if existing_headers == headers: + # QgsMessageLog.logMessage(f"Column headers already exist in {sheetname}.", "STDM dev") + # else: + # # add missing column headers + # for header in headers: + # if header not in existing_headers: + # sheet.insert_cols(1) + # sheet.cell(row=1, column=1, value=header) + # # save the workbook + # workbook.save(self.form) + # QgsMessageLog.logMessage(f"Column headers added to {sheetname}.", "STDM dev") + # else: + # # create new workbook with sheets and column headers + # workbook = Workbook() + # for sheetname, headers in sheets.items(): + # sheet = workbook.create_sheet(sheetname) + # sheet.append(headers) + # # delete default first sheet if it exists + # if 'Sheet' in workbook.sheetnames: + # sheet = workbook['Sheet'] + # workbook.remove(sheet) + # QgsMessageLog.logMessage("Default sheet 'Sheet' removed from the workbook.", "STDM dev") + # # save the workbook + # workbook.save(self.form) + # QgsMessageLog.logMessage(f"{self.form} created with sheets: " + # f"{', '.join(sheets.keys())} and column headers.", "STDM dev") + # return True, "" + # + # except Exception as e: + # QgsMessageLog.logMessage(f"An error occurred: {e}", "STDM dev") + # reason = f"An error occurred: {e}" + # return failed, reason + + try: + # check if file exists + if os.path.isfile(self.form): + # file exists, load workbook + workbook = load_workbook(self.form) + # delete default first sheet + workbook.remove(workbook.active) + for sheetname, headers in sheets.items(): + # check if sheet exists + if sheetname in workbook.sheetnames: + sheet = workbook[sheetname] + QgsMessageLog.logMessage(f"{sheetname} already exists in {self.form}.", "STDM dev") + else: + # create new sheet + sheet = workbook.create_sheet(sheetname) + # add column headers + sheet.append(headers) + # save the workbook + workbook.save(self.form) + QgsMessageLog.logMessage(f"{sheetname} created in {self.form} with column headers: " + f"{', '.join(headers)}.", "STDM dev") + # check if headers exist + if sheet.max_row == 1: + existing_headers = [cell.value for cell in sheet[1]] + if existing_headers == headers: + QgsMessageLog.logMessage(f"Column headers already exist in {sheetname}.", "STDM dev") + else: + # add missing column headers + for header in headers: + if header not in existing_headers: + sheet.insert_cols(1) + sheet.cell(row=1, column=1, value=header) + # save the workbook + workbook.save(self.form) + QgsMessageLog.logMessage(f"Column headers added to {sheetname}.", "STDM dev") + else: + QgsMessageLog.logMessage(f"Cannot check headers in {sheetname} as it already has data.", + "STDM dev") + else: + # create new workbook with sheets and column headers + workbook = Workbook() + # delete default first sheet + workbook.remove(workbook.active) + for sheetname, headers in sheets.items(): + sheet = workbook.create_sheet(sheetname) + sheet.append(headers) + # save the workbook + workbook.save(self.form) + QgsMessageLog.logMessage(f"{self.form} created with sheets: " + f"{', '.join(sheets.keys())} and column headers.", "STDM dev") + return True, "" + except Exception as e: + QgsMessageLog.logMessage(f"An error occurred: {e}", "STDM dev") + return failed, f"An error occurred: {e}" + + +class ProviderWriter(XLSFormWriter): + + def __init__(self, stdm_profile, entities): + self.entities = entities + self.profile = stdm_profile + self.provider_file_name = self.profile.name.replace(' ', '_') + QgsMessageLog.logMessage("STDM profile name {profile}".format(profile=self.provider_file_name), + "STDM dev") + + XLSFormWriter.__init__(self, self.provider_file_name) + + def create_xsl_file(self) -> tuple[bool, str]: + return self.create_xls_form() + + def write_data_to_xlsform(self) -> tuple[bool, str]: + + entities = OrderedDict() + + if self.form is None: + QgsMessageLog.logMessage("Error: No file to write data!", "STDM dev") + return False, "Error: No file to write data!" + + for entity_name in self.entities: + reader = self.create_entity_reader(entity_name) + entity_values = reader.read_attributes() + for col, col_type in entity_values.items(): + if reader.on_column_info(col): + col_attributes = reader.format_lookup_items(col) + entities[col] = {col_type: col_attributes} + else: + entities[col] = col_type + + row_data = {'survey': [], + 'choices': []} + + for col, val in entities.items(): + + # QgsMessageLog.logMessage("Entities col={col}, val={val}".format(col=col, val=val), "STDM dev") + + if val == 'VARCHAR': + row = OrderedDict() + row['type'] = 'text' + row['name'] = col + row_data['survey'].append(row) + + elif val == 'INT' or val == 'DOUBLE' or val == 'AUTO_GENERATED': + row = OrderedDict() + row['type'] = 'integer' + row['name'] = col + row_data['survey'].append(row) + + elif val == 'DATE': + row = OrderedDict() + row['type'] = 'date' + row['name'] = col + row_data['survey'].append(row) + + elif val == 'GEOMETRY': + row = OrderedDict() + row['type'] = 'geoshape' + row['name'] = col + row_data['survey'].append(row) + + elif isinstance(val, dict): + row = OrderedDict() + + data_type = list(val)[0] + # QgsMessageLog.logMessage(f"{data_type}", "STDM dev") + if data_type == 'LOOKUP': + row['type'] = 'select_one {col}'.format(col=col) + row['name'] = col + row_data['survey'].append(row) + elif data_type == 'MULTIPLE_SELECT': + row['type'] = 'select_multiple {col}'.format(col=col) + row['name'] = col + row_data['survey'].append(row) + elif data_type == 'ADMIN_SPATIAL_UNIT': + row['type'] = 'select_multiple {col}'.format(col=col) + row['name'] = col + row_data['survey'].append(row) + + for v in val.values(): + row['list_name'] = col + row['name'] = v + row_data['choices'].append(row) + + QgsMessageLog.logMessage("Rows: rows={rows}".format(rows=row_data), "STDM dev") + + # load the workbook + workbook = load_workbook(self.form) + + # delete default first sheet + sheets = {'survey': ['type', 'name', 'label'], + 'choices': ['list_name', 'name', 'label'], + 'settings': ['form_title', 'form_id', 'version', 'instance_name']} + + if 'Sheet' in workbook.sheetnames: + sheet = workbook['Sheet'] + workbook.remove(sheet) + print("Default sheet 'Sheet' removed from the workbook.") + + for sheetname, headers in sheets.items(): + # check if sheet exists + if sheetname in workbook.sheetnames: + sheet = workbook[sheetname] + print(f"{sheetname} already exists in {self.form}.") + else: + print(f"{sheetname} does not exist in {self.form}.") + continue + + # add missing column headers + existing_headers = [cell.value for cell in sheet[1]] + for header in headers: + if header not in existing_headers: + sheet.insert_cols(1) + sheet.cell(row=1, column=1, value=header) + print(f"{header} added to {sheetname}.") + + # add data to the sheet + for _row_data in row_data.get(sheetname, []): + current_row = [] + + QgsMessageLog.logMessage("Headers: {h} ".format(h=headers), "STDM dev") + row = [] + flag = False + for header in headers: + QgsMessageLog.logMessage("Raw values: {r} header {h}".format(r=_row_data, h=header), "STDM dev") + column_numbers = [i for i, x in enumerate(existing_headers) if x == header] + # if len(column_numbers) == 1: + + QgsMessageLog.logMessage("In row data: {d}".format(d=_row_data), + "STDM dev") + for type, data in _row_data.items(): + QgsMessageLog.logMessage("Processing dict: {d}, {v}".format(d=type, v=data), + "STDM dev") + if isinstance(data, dict): + # if type == 'type': + # row.append(data) + # sheet.append(row) + for v in data.keys(): + choice_row = ['name', v] + QgsMessageLog.logMessage("Raw values after looping: {r} header {h}" + .format(r=choice_row, h=header), "STDM dev") + sheet.append(choice_row) + sheet.cell(row=len(sheet['A']) + 1, column=column_numbers[0] + 1, + value=row_data.get(header)) + QgsMessageLog.logMessage("Raw data: {d}".format(d=choice_row), + "STDM dev") + flag = True + else: + if header == type: + row.append(data) + QgsMessageLog.logMessage("Raw data: {d}".format(d=row), + "STDM dev") + + if not flag: + sheet.append(row) + # sheet.cell(row=len(sheet['A']) + 1, column=column_numbers[0] + 1, + # value=row_data.get(header)) + + # else: + # row = [] + # flag = False + # QgsMessageLog.logMessage("In row data: {d}".format(d=_row_data), + # "STDM dev") + # for type, data in _row_data.items(): + # QgsMessageLog.logMessage("Processing dict: {d}, {v}".format(d=type, v=data), + # "STDM dev") + # if isinstance(data, dict): + # for v in data.keys(): + # choice_row = ['name', v] + # QgsMessageLog.logMessage("Raw values after looping: {r} header {h}" + # .format(r=choice_row, h=header), "STDM dev") + # sheet.append(choice_row) + # sheet.cell(row=len(sheet['A'])+1, column=column_numbers[-1]+1, value=row_data.get(header)) + # QgsMessageLog.logMessage("Raw data: {d}".format(d=choice_row), + # "STDM dev") + # flag = True + # else: + # row.append(row.append(data)) + # QgsMessageLog.logMessage("Raw data: {d}".format(d=row), + # "STDM dev") + # if not flag: + # sheet.append(row) + # sheet.cell(row=len(sheet['A'])+1, column=column_numbers[-1]+1, value=row_data.get(header)) + + # QgsMessageLog.logMessage("Current row {r}".format(r=current_row), "STDM dev") + + # if isinstance(_row_data, dict): + # for k, v in _row_data.items(): + # row.append(v) + # # row.append(_row_data.get(header)) + # sheet.append(row) + # else: + # sheet.append(row) + print(f"Example data added to {sheetname}.") + + # save the workbook + workbook.save(self.form) + print(f"Data saved to {self.form}.") + + return True, "File found" + + def create_entity_reader(self, entity): + """ + Initialize the reader class after each entity to avoid + redundant data + """ + self.entity_read = EntityReader(entity) + return self.entity_read diff --git a/stdm/ui/mobile_data_provider/mobile_provider_resgirty.py b/stdm/ui/mobile_data_provider/mobile_provider_resgirty.py new file mode 100644 index 00000000..e69de29b diff --git a/stdm/ui/mobile_data_provider/moble_provider_container_loader.py b/stdm/ui/mobile_data_provider/moble_provider_container_loader.py new file mode 100644 index 00000000..e69de29b diff --git a/stdm/ui/mobile_data_provider/ui_mobile_provider_column_editor.ui b/stdm/ui/mobile_data_provider/ui_mobile_provider_column_editor.ui new file mode 100644 index 00000000..a96da444 --- /dev/null +++ b/stdm/ui/mobile_data_provider/ui_mobile_provider_column_editor.ui @@ -0,0 +1,182 @@ + + + ColumnEditor + + + + 0 + 0 + 455 + 560 + + + + + 0 + 0 + + + + + 16777215 + 560 + + + + Column editor + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + 0 + 0 + + + + Advance + + + true + + + false + + + + + + Condition + + + + + + + + + + Edit + + + + + + + Validate + + + + + + + + + + + 0 + 0 + + + + Edit + + + + + + + Filter + + + + + + + + + + Edit + + + + + + + + + + + + + + + Expression + + + + + + + Appearance + + + + + + + Required + + + + + + + Default + + + + + + + + + + + + + + + + + + 8 + + + 0 + + + + + + + + QgsCollapsibleGroupBox + QWidget +
qgscollapsiblegroupbox.h
+
+
+ + +
diff --git a/stdm/ui/mobile_data_provider/ui_mobile_provider_wizard.ui b/stdm/ui/mobile_data_provider/ui_mobile_provider_wizard.ui new file mode 100644 index 00000000..790a3eb3 --- /dev/null +++ b/stdm/ui/mobile_data_provider/ui_mobile_provider_wizard.ui @@ -0,0 +1,334 @@ + + + MobileProviderWizard + + + Qt::WindowModal + + + + 0 + 0 + 836 + 629 + + + + + 0 + 0 + + + + + 720 + 100 + + + + + 0 + 460 + + + + Mobile Provider Wizard + + + true + + + QWizard::ModernStyle + + + QWizard::HaveCustomButton1|QWizard::HelpButtonOnRight + + + + + 75 + true + + + + Mobile Provider + + + This wizard will guide you through the process of exporting to the default data provider + + + + + + Introduction + + + + + + + 14 + 75 + true + + + + QFrame::NoFrame + + + QFrame::Raised + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:14pt; font-weight:600; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" vertical-align:sub;">This wizard will enable you to export STDM native formats to a data provider format.</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; vertical-align:sub;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" vertical-align:sub;">Some of the current supported data providers include ODK, Kobo, QFields and Trimple</span></p></body></html> + + + + + + + + + + + Mobile Provider Appearance Customization + + + Edit column appearance before export + + + + + + + 0 + 0 + + + + Qt::Vertical + + + + + 100 + 100 + + + + Qt::Horizontal + + + + + 0 + 0 + + + + Entities + + + + 5 + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 0 + 1 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + QAbstractItemView::SelectRows + + + + + + + + + 0 + 0 + + + + Columns + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 1 + 1 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + 130 + + + true + + + 30 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 100 + 25 + + + + + 30 + 25 + + + + Edit Appearance + + + + 20 + 20 + + + + + + + + + + + + + + + + Export STDM Profile Data to Mobile Provider + + + Click finish to start the export process + + + + + + Save status will be displayed in the window below. + + + + + + + + 0 + 1 + + + + true + + + + + + + + txtMobileProviderIntroduction + providerEntities + btnEditApperance + providerEntityColumns + txtHtml + + + +