From 1b383a54fb94e447bce4884f1677c14bd35eee4a Mon Sep 17 00:00:00 2001 From: Erick Opiyo Date: Wed, 5 Apr 2023 12:58:09 +0300 Subject: [PATCH 1/4] content edit changes --- stdm/plugin.py | 93 +++++ stdm/ui/mobile_data_provider/__init__.py | 0 .../base_mobile_provider.py | 48 +++ .../mobile_provider_resgirty | 0 .../moble_provider_container_loader.py | 0 .../ui_mobile_provider_wizard.ui | 325 ++++++++++++++++++ 6 files changed, 466 insertions(+) create mode 100644 stdm/ui/mobile_data_provider/__init__.py create mode 100644 stdm/ui/mobile_data_provider/base_mobile_provider.py create mode 100644 stdm/ui/mobile_data_provider/mobile_provider_resgirty create mode 100644 stdm/ui/mobile_data_provider/moble_provider_container_loader.py create mode 100644 stdm/ui/mobile_data_provider/ui_mobile_provider_wizard.ui diff --git a/stdm/plugin.py b/stdm/plugin.py index 91187a2eb..7f187c947 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,12 @@ def mobile_form_importer(self): importer_dialog = ProfileInstanceRecords(self.iface.mainWindow()) importer_dialog.exec_() + def mobile_provider_export_form_generator(self, provider): + BaseMobileProvider(self.iface.mainWindow(), provider) + + 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 000000000..e69de29bb 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 000000000..b476d06df --- /dev/null +++ b/stdm/ui/mobile_data_provider/base_mobile_provider.py @@ -0,0 +1,48 @@ +""" +/*************************************************************************** +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 ( + pyqtSignal +) +from qgis.PyQt.QtWidgets import ( + QWizard, + QMessageBox +) + +from stdm.ui.gui_utils import GuiUtils + +WIDGET, BASE = uic.loadUiType( + GuiUtils.get_ui_file_path('mobile_data_provider/ui_mobile_provider_wizard.ui')) + + +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.msg = QMessageBox() + self.msg.setWindowTitle("Base Mobile Provider") + self.setupUi(self) + self.provider = provider + self.msg.setText("{provider}".format(provider=self.provider)) + self.msg.exec_() diff --git a/stdm/ui/mobile_data_provider/mobile_provider_resgirty b/stdm/ui/mobile_data_provider/mobile_provider_resgirty new file mode 100644 index 000000000..e69de29bb 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 000000000..e69de29bb 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 000000000..72efcfae7 --- /dev/null +++ b/stdm/ui/mobile_data_provider/ui_mobile_provider_wizard.ui @@ -0,0 +1,325 @@ + + + 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 + + + + + + + + + + + Entity Customization + + + Add or edit entity columns, lookups and lookup values + + + + + + + 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 + + + + + + + + + 30 + 25 + + + + + 30 + 25 + + + + + + + + 20 + 20 + + + + + + + + + + + + + + + + Save configuration + + + Click finish to save changes in your configuration to the database. + + + + + + Save status will be displayed in the window below. + + + + + + + + 0 + 1 + + + + true + + + + + + + + txtMobileProviderIntroduction + lvEntities + btnEditApperance + tbvColumns + txtHtml + + + + From 52c0db4a4ed1b2bfe448f43fb3da26e4f9b43406 Mon Sep 17 00:00:00 2001 From: Erick Opiyo Date: Tue, 11 Apr 2023 19:19:12 +0300 Subject: [PATCH 2/4] Auto stash before rebase of "master" added odk columns --- .gitignore | 2 + stdm/plugin.py | 3 +- .../base_mobile_provider.py | 226 +++++++- .../mobile_data_provider/custom_item_model.py | 493 ++++++++++++++++++ ...r_resgirty => mobile_provider_resgirty.py} | 0 .../ui_mobile_provider_wizard.ui | 17 +- 6 files changed, 729 insertions(+), 12 deletions(-) create mode 100644 stdm/ui/mobile_data_provider/custom_item_model.py rename stdm/ui/mobile_data_provider/{mobile_provider_resgirty => mobile_provider_resgirty.py} (100%) diff --git a/.gitignore b/.gitignore index b16cc73dd..bbbd3e6f2 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 7f187c947..c8fd1a755 100644 --- a/stdm/plugin.py +++ b/stdm/plugin.py @@ -2285,7 +2285,8 @@ def mobile_form_importer(self): importer_dialog.exec_() def mobile_provider_export_form_generator(self, provider): - BaseMobileProvider(self.iface.mainWindow(), provider) + base_mobile_provider_wizard = BaseMobileProvider(self.iface.mainWindow(), provider) + base_mobile_provider_wizard.exec_() def mobile_provider_import_form_generator(self): pass diff --git a/stdm/ui/mobile_data_provider/base_mobile_provider.py b/stdm/ui/mobile_data_provider/base_mobile_provider.py index b476d06df..efe19cb32 100644 --- a/stdm/ui/mobile_data_provider/base_mobile_provider.py +++ b/stdm/ui/mobile_data_provider/base_mobile_provider.py @@ -19,14 +19,25 @@ from qgis.PyQt import uic from qgis.PyQt.QtCore import ( - pyqtSignal + pyqtSignal, + Qt, + QSortFilterProxyModel ) from qgis.PyQt.QtWidgets import ( QWizard, - QMessageBox + QAbstractItemView, ) +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 stdm.ui.wizard.column_editor import ColumnEditor +from .custom_item_model import ( + EntitiesModel, + ColumnEntitiesModel, +) +from stdm.utils.util import enable_drag_sort WIDGET, BASE = uic.loadUiType( GuiUtils.get_ui_file_path('mobile_data_provider/ui_mobile_provider_wizard.ui')) @@ -40,9 +51,210 @@ class BaseMobileProvider(WIDGET, BASE): def __init__(self, parent, provider=None): QWizard.__init__(self, parent) - self.msg = QMessageBox() - self.msg.setWindowTitle("Base Mobile Provider") self.setupUi(self) - self.provider = provider - self.msg.setText("{provider}".format(provider=self.provider)) - self.msg.exec_() + + # 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 + _, entity = self._get_entity(self.providerEntities) + + profile = self.current_profile() + params = {} + params['parent'] = self + params['column'] = column + params['entity'] = entity + params['profile'] = profile + params['entity_has_records'] = self.entity_has_records(entity) + params['is_new'] = False + + original_column = column + + editor = ColumnEditor(**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) + + 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) \ No newline at end of file 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 000000000..efdca5c1f --- /dev/null +++ b/stdm/ui/mobile_data_provider/custom_item_model.py @@ -0,0 +1,493 @@ +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'] + } + + 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] + + 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_resgirty b/stdm/ui/mobile_data_provider/mobile_provider_resgirty.py similarity index 100% rename from stdm/ui/mobile_data_provider/mobile_provider_resgirty rename to stdm/ui/mobile_data_provider/mobile_provider_resgirty.py diff --git a/stdm/ui/mobile_data_provider/ui_mobile_provider_wizard.ui b/stdm/ui/mobile_data_provider/ui_mobile_provider_wizard.ui index 72efcfae7..28c4229c8 100644 --- a/stdm/ui/mobile_data_provider/ui_mobile_provider_wizard.ui +++ b/stdm/ui/mobile_data_provider/ui_mobile_provider_wizard.ui @@ -78,6 +78,15 @@ 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> + @@ -141,7 +150,7 @@ 5 - + 0 @@ -191,7 +200,7 @@ 5 - + 1 @@ -315,9 +324,9 @@ txtMobileProviderIntroduction - lvEntities + providerEntities btnEditApperance - tbvColumns + providerEntityColumns txtHtml From 138814954468043d516c3a8738b9820b0cdc51a0 Mon Sep 17 00:00:00 2001 From: Erick Opiyo Date: Thu, 13 Apr 2023 14:05:27 +0300 Subject: [PATCH 3/4] latest --- .../base_mobile_provider.py | 47 +- stdm/ui/mobile_data_provider/column_editor.py | 988 ++++++++++++++++++ .../ui_mobile_provider_column_editor.ui | 137 +++ .../ui_mobile_provider_wizard.ui | 16 +- 4 files changed, 1177 insertions(+), 11 deletions(-) create mode 100644 stdm/ui/mobile_data_provider/column_editor.py create mode 100644 stdm/ui/mobile_data_provider/ui_mobile_provider_column_editor.ui diff --git a/stdm/ui/mobile_data_provider/base_mobile_provider.py b/stdm/ui/mobile_data_provider/base_mobile_provider.py index efe19cb32..aa867e23c 100644 --- a/stdm/ui/mobile_data_provider/base_mobile_provider.py +++ b/stdm/ui/mobile_data_provider/base_mobile_provider.py @@ -32,12 +32,13 @@ from stdm.data.configuration.profile import Profile from stdm.settings import current_profile from stdm.ui.gui_utils import GuiUtils -from stdm.ui.wizard.column_editor import ColumnEditor +from .column_editor import ColumnEditor from .custom_item_model import ( EntitiesModel, ColumnEntitiesModel, ) from stdm.utils.util import enable_drag_sort +from ...data.pg_utils import pg_table_exists, pg_table_record_count WIDGET, BASE = uic.loadUiType( GuiUtils.get_ui_file_path('mobile_data_provider/ui_mobile_provider_wizard.ui')) @@ -61,7 +62,7 @@ def __init__(self, parent, provider=None): ) self.register_fields() - self.txtMobileProviderIntroduction.setText(self.current_profile().name) + # self.txtMobileProviderIntroduction.setText(self.current_profile().name) self.entity_model = EntitiesModel() self.set_views_entity_model(self.entity_model) self.init_entity_item_model() @@ -257,4 +258,44 @@ def set_model_items_selectable(self): 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) \ No newline at end of file + #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 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 000000000..9671bbe5c --- /dev/null +++ b/stdm/ui/mobile_data_provider/column_editor.py @@ -0,0 +1,988 @@ +# -*- 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.exceptions import DummyException +from stdm.data.configuration.columns import BaseColumn +from stdm.data.configuration.columns import ForeignKeyColumn +from stdm.data.configuration.entity_relation import EntityRelation +from stdm.data.pg_utils import vector_layer +from stdm.ui.gui_utils import GuiUtils +from stdm.ui.notification import NotificationBar +from stdm.ui.wizard.bigint_property import BigintProperty +from stdm.ui.wizard.code_property import CodeProperty +from stdm.ui.wizard.date_property import DateProperty +from stdm.ui.wizard.double_property import DoubleProperty +from stdm.ui.wizard.dtime_property import DTimeProperty +from stdm.ui.wizard.expression_property import ExpressionProperty +from stdm.ui.wizard.fk_property import FKProperty +from stdm.ui.wizard.geometry_property import GeometryProperty +from stdm.ui.wizard.lookup_property import LookupProperty +from stdm.ui.wizard.multi_select_property import MultiSelectProperty +from stdm.ui.wizard.varchar_property import VarcharProperty + +WIDGET, BASE = uic.loadUiType( + GuiUtils.get_ui_file_path('mobile_data_provider/ui_mobile_provider_wizard.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 ColumnEditor(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() + + # 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 + + self.prev_column = self.column + + # 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 = \ + [str(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: str): + """ + 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(QDialogButtonBox.Ok).clicked.connect(self.accept) + self.buttonBox.button(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) + + # Dont allow mandatory fields if an entity already has records. + if self.entity_has_records: + self.cbMandt.setEnabled(False) + + self.cbIndex.setVisible(False) + + 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:] + locale = (QSettings().value("locale/userLocale") or 'en-US')[0:2] + + # if locale == 'en': + 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] + + # 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: BaseColumn): + """ + 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 str(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'] = str(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 DummyException: + 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().items(): + # 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('property' in self.type_attribs[ti]) + 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 DummyException: + return '' + + def fill_work_area(self): + """ + Sets work area 'form_fields' with form control values + """ + self.form_fields['colname'] = str(self.edtColName.text()) + self.form_fields['description'] = str(self.edtColDesc.text()) + self.form_fields['label'] = str(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'] = str(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 = str(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("'{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 + if isinstance(self.prev_column, ForeignKeyColumn): + if self.prev_column.display_name() != new_column.display_name(): + self.entity.remove_column(self.prev_column.name) + self.entity.add_column(new_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) 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 000000000..d47d722dd --- /dev/null +++ b/stdm/ui/mobile_data_provider/ui_mobile_provider_column_editor.ui @@ -0,0 +1,137 @@ + + + ColumnEditor + + + + 0 + 0 + 359 + 336 + + + + + 0 + 0 + + + + + 16777215 + 560 + + + + Column editor + + + + + + + + + 8 + + + 0 + + + + + + + + 0 + 0 + + + + Expression + + + + + + + + 0 + 0 + + + + Qt::LeftToRight + + + Required + + + false + + + + + + + + 0 + 0 + + + + false + + + QComboBox::InsertAlphabetically + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + 0 + 0 + + + + Default + + + + + + + Appearance + + + + + + + Qt::Horizontal + + + + + + + cboDataType + + + + diff --git a/stdm/ui/mobile_data_provider/ui_mobile_provider_wizard.ui b/stdm/ui/mobile_data_provider/ui_mobile_provider_wizard.ui index 28c4229c8..790a3eb35 100644 --- a/stdm/ui/mobile_data_provider/ui_mobile_provider_wizard.ui +++ b/stdm/ui/mobile_data_provider/ui_mobile_provider_wizard.ui @@ -94,12 +94,12 @@ p, li { white-space: pre-wrap; } - + - Entity Customization + Mobile Provider Appearance Customization - Add or edit entity columns, lookups and lookup values + Edit column appearance before export @@ -261,7 +261,7 @@ p, li { white-space: pre-wrap; } - 30 + 100 25 @@ -272,7 +272,7 @@ p, li { white-space: pre-wrap; } - + Edit Appearance @@ -291,12 +291,12 @@ p, li { white-space: pre-wrap; } - + - Save configuration + Export STDM Profile Data to Mobile Provider - Click finish to save changes in your configuration to the database. + Click finish to start the export process From cb2d55223eacfea83f4f886bba0f6152a8bbe2dc Mon Sep 17 00:00:00 2001 From: Erick Opiyo Date: Mon, 22 May 2023 13:36:36 +0300 Subject: [PATCH 4/4] added xlsforms --- .../base_mobile_provider.py | 169 +++- stdm/ui/mobile_data_provider/column_editor.py | 813 ++---------------- .../mobile_data_provider/custom_item_model.py | 56 +- .../mobile_provider_entity_reader.py | 104 +++ .../mobile_provider_export.py | 446 ++++++++++ .../ui_mobile_provider_column_editor.ui | 217 +++-- 6 files changed, 959 insertions(+), 846 deletions(-) create mode 100644 stdm/ui/mobile_data_provider/mobile_provider_entity_reader.py create mode 100644 stdm/ui/mobile_data_provider/mobile_provider_export.py diff --git a/stdm/ui/mobile_data_provider/base_mobile_provider.py b/stdm/ui/mobile_data_provider/base_mobile_provider.py index aa867e23c..d3e880f71 100644 --- a/stdm/ui/mobile_data_provider/base_mobile_provider.py +++ b/stdm/ui/mobile_data_provider/base_mobile_provider.py @@ -19,30 +19,59 @@ from qgis.PyQt import uic from qgis.PyQt.QtCore import ( - pyqtSignal, 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 ColumnEditor +from .column_editor import AppearanceColumnEditor from .custom_item_model import ( EntitiesModel, ColumnEntitiesModel, ) from stdm.utils.util import enable_drag_sort -from ...data.pg_utils import pg_table_exists, pg_table_record_count +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): """ @@ -135,36 +164,43 @@ def edit_column(self): rid, column, model_item = self.get_selected_item_data(self.providerEntityColumns) if rid == -1: return - _, entity = self._get_entity(self.providerEntities) + 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) - profile = self.current_profile() - params = {} - params['parent'] = self - params['column'] = column - params['entity'] = entity - params['profile'] = profile - params['entity_has_records'] = self.entity_has_records(entity) - params['is_new'] = False + params['is_new'] = False - original_column = column + original_column = column # model_item.entity(column.name) - editor = ColumnEditor(**params) - result = editor.exec_() + editor = AppearanceColumnEditor(**params) + result = editor.exec_() - if result == 1: + 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_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.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) + model_item.edit_entity(original_column, editor.column) - entity.columns[original_column.name] = editor.column - entity.rename_column(original_column.name, editor.column.name) + 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): """ @@ -299,3 +335,86 @@ def entity_has_records(self, entity): 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 index 9671bbe5c..d0c308d83 100644 --- a/stdm/ui/mobile_data_provider/column_editor.py +++ b/stdm/ui/mobile_data_provider/column_editor.py @@ -40,27 +40,12 @@ QMessageBox ) -from stdm.exceptions import DummyException from stdm.data.configuration.columns import BaseColumn -from stdm.data.configuration.columns import ForeignKeyColumn -from stdm.data.configuration.entity_relation import EntityRelation -from stdm.data.pg_utils import vector_layer from stdm.ui.gui_utils import GuiUtils from stdm.ui.notification import NotificationBar -from stdm.ui.wizard.bigint_property import BigintProperty -from stdm.ui.wizard.code_property import CodeProperty -from stdm.ui.wizard.date_property import DateProperty -from stdm.ui.wizard.double_property import DoubleProperty -from stdm.ui.wizard.dtime_property import DTimeProperty -from stdm.ui.wizard.expression_property import ExpressionProperty -from stdm.ui.wizard.fk_property import FKProperty -from stdm.ui.wizard.geometry_property import GeometryProperty -from stdm.ui.wizard.lookup_property import LookupProperty -from stdm.ui.wizard.multi_select_property import MultiSelectProperty -from stdm.ui.wizard.varchar_property import VarcharProperty WIDGET, BASE = uic.loadUiType( - GuiUtils.get_ui_file_path('mobile_data_provider/ui_mobile_provider_wizard.ui')) + GuiUtils.get_ui_file_path('mobile_data_provider/ui_mobile_provider_column_editor.ui')) LOGGER = logging.getLogger('stdm') LOGGER.setLevel(logging.DEBUG) @@ -72,7 +57,7 @@ ] -class ColumnEditor(WIDGET, BASE): +class AppearanceColumnEditor(WIDGET, BASE): """ Dialog to add/edit entity columns """ @@ -117,332 +102,12 @@ def __init__(self, **kwargs): 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 - - self.prev_column = self.column - - # 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 = \ - [str(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.appearanceColumnDataType.currentIndexChanged.connect(self.change_data_type) self.notice_bar = NotificationBar(self.notif_bar) + self._exclude_col_type_info = [] 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: str): - """ - 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(QDialogButtonBox.Ok).clicked.connect(self.accept) - self.buttonBox.button(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) - - # Dont allow mandatory fields if an entity already has records. - if self.entity_has_records: - self.cbMandt.setEnabled(False) - - self.cbIndex.setVisible(False) - - 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:] - locale = (QSettings().value("locale/userLocale") or 'en-US')[0:2] - - # if locale == 'en': - 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] - - # 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: BaseColumn): - """ - 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 @@ -457,16 +122,16 @@ def init_type_attribs(self): '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} + '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, - 'property': self.bigint_property} + 'minimum': 0, 'maximum': 0 + } self.type_attribs['TEXT'] = { 'mandt': {'check_state': False, 'enabled_state': True}, @@ -481,8 +146,7 @@ def init_type_attribs(self): '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} + 'precision': 18, 'scale': 6} self.type_attribs['DATE'] = { 'mandt': {'check_state': False, 'enabled_state': True}, @@ -492,8 +156,8 @@ def init_type_attribs(self): 'minimum': datetime.date.min, 'maximum': datetime.date.max, 'min_use_current_date': False, - 'max_use_current_date': False, - 'property': self.date_property} + 'max_use_current_date': False + } self.type_attribs['DATETIME'] = { 'mandt': {'check_state': False, 'enabled_state': True}, @@ -503,33 +167,21 @@ def init_type_attribs(self): '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} + '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}, - 'entity_relation': {}, - 'property': self.lookup_property, 'prop_set': 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}, - 'srid': "", 'geom_type': 0, - 'property': self.geometry_property, 'prop_set': False} + 'index': {'check_state': False, 'enabled_state': False} + } self.type_attribs['BOOL'] = { 'mandt': {'check_state': False, 'enabled_state': False}, @@ -548,16 +200,15 @@ def init_type_attribs(self): '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} + '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}, - 'first_parent': None, 'second_parent': self.entity, - 'property': self.multi_select_property, 'prop_set': False} + 'index': {'check_state': False, 'enabled_state': False} + } self.type_attribs['AUTO_GENERATED'] = { 'mandt': {'check_state': False, 'enabled_state': True}, @@ -567,281 +218,82 @@ def init_type_attribs(self): '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} + '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} + 'index': {'check_state': False, 'enabled_state': True} + } - def data_type_property(self): + def init_controls(self): """ - Executes the function assigned to the property attribute of - the current selected data type. + Initialize GUI controls default state when the dialog window is opened. """ - self.type_attribs[self.current_type_info()]['property']() + self.populate_data_type_cbo() - 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() + self.buttonBox.button(QDialogButtonBox.Ok).clicked.connect(self.accept) + self.buttonBox.button(QDialogButtonBox.Cancel).clicked.connect(self.cancel) - def bigint_property(self): + def populate_data_type_cbo(self): """ - Opens a property editor for the BigInt data type. + Fills the data type combobox widget with BaseColumn type names. """ - 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() + 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'] + } - 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 + col_type = self._column_type_info(self.column) - # 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 str(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'] = str(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() + if col_type == 'VARCHAR': + for appearance in appearance_dictionary['text']: + self.appearanceColumnDataType.addItem(appearance) - 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 + elif col_type == 'INT': + for appearance in appearance_dictionary['integer']: + self.appearanceColumnDataType.addItem(appearance) - 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() + elif col_type == 'DOUBLE': + for appearance in appearance_dictionary['decimal']: + self.appearanceColumnDataType.addItem(appearance) - 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) + elif col_type == 'DATE': + for appearance in appearance_dictionary['date']: + self.appearanceColumnDataType.addItem(appearance) - def property_by_name(self, ti, name): - try: - return self.dtype_property(ti)['property'][name] - except DummyException: - return None + elif col_type == 'LOOKUP': + for appearance in appearance_dictionary['select_one']: + self.appearanceColumnDataType.addItem(appearance) - def populate_data_type_cbo(self): - """ - Fills the data type combobox widget with BaseColumn type names. - """ - self.cboDataType.clear() + elif col_type == 'MULTIPLE_SELECT': + for appearance in appearance_dictionary['select_multiple']: + self.appearanceColumnDataType.addItem(appearance) - for name, col in BaseColumn.types_by_display_name().items(): - # Specify columns to exclude - if col.TYPE_INFO not in self._exclude_col_type_info: - self.cboDataType.addItem(name) + elif col_type == 'GEOMETRY': + for appearance in appearance_dictionary['geopoint']: + self.appearanceColumnDataType.addItem(appearance) - if self.cboDataType.count() > 0: - self.cboDataType.setCurrentIndex(0) + 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.cboDataType.itemText(index) + text = self.appearanceColumnDataType.itemText(index) col_cls = BaseColumn.types_by_display_name().get(text, None) if col_cls is None: return @@ -853,11 +305,9 @@ def change_data_type(self, index): self.notice_bar.insertErrorNotification(msg) return - self.btnColProp.setEnabled('property' in self.type_attribs[ti]) self.type_info = ti opts = self.type_attribs[ti] self.set_optionals(opts) - self.set_min_max_defaults(ti) def set_optionals(self, opts): """ @@ -866,123 +316,22 @@ def set_optionals(self, opts): 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'])) + pass - 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): + def _column_type_info(self, column): """ - Returns a TYPE_INFO of a data type - :rtype: str + Check if column has TYPE_INFO attribute + :param column: Entity column object + :return: Column type. Otherwise None + :rtype: String or None """ - text = self.cboDataType.itemText(self.cboDataType.currentIndex()) try: - return BaseColumn.types_by_display_name()[text].TYPE_INFO - except DummyException: - return '' - - def fill_work_area(self): - """ - Sets work area 'form_fields' with form control values - """ - self.form_fields['colname'] = str(self.edtColName.text()) - self.form_fields['description'] = str(self.edtColDesc.text()) - self.form_fields['label'] = str(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'] = str(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 = str(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("'{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 - if isinstance(self.prev_column, ForeignKeyColumn): - if self.prev_column.display_name() != new_column.display_name(): - self.entity.remove_column(self.prev_column.name) - self.entity.add_column(new_column) - self.column = new_column - self.done(1) + return column.TYPE_INFO + except AttributeError: + return None 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) + 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 index efdca5c1f..aec42aab6 100644 --- a/stdm/ui/mobile_data_provider/custom_item_model.py +++ b/stdm/ui/mobile_data_provider/custom_item_model.py @@ -210,7 +210,11 @@ def __init__(self, parent=None): 'Single Select Lookup': ['select_one', 'none'], 'Date': ['date', 'none'], 'Multiple Select Lookup': ['select_multiple', 'none'], - 'Geometry': ['geoshape', '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) @@ -231,6 +235,54 @@ def _add_row(self, entity): 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( @@ -408,8 +460,6 @@ def model_item(self, row: int) -> LookupEntityModelItem: return self.item(row) - - ################ # Social Tenure Relationship model item class STREntityModelItem(QStandardItem): 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 000000000..266abc971 --- /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 000000000..b340964cf --- /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/ui_mobile_provider_column_editor.ui b/stdm/ui/mobile_data_provider/ui_mobile_provider_column_editor.ui index d47d722dd..a96da4442 100644 --- a/stdm/ui/mobile_data_provider/ui_mobile_provider_column_editor.ui +++ b/stdm/ui/mobile_data_provider/ui_mobile_provider_column_editor.ui @@ -6,8 +6,8 @@ 0 0 - 359 - 336 + 455 + 560 @@ -26,71 +26,7 @@ Column editor - - - - - - - 8 - - - 0 - - - - - - - - 0 - 0 - - - - Expression - - - - - - - - 0 - 0 - - - - Qt::LeftToRight - - - Required - - - false - - - - - - - - 0 - 0 - - - - false - - - QComboBox::InsertAlphabetically - - - - - - - + Qt::Horizontal @@ -100,38 +36,147 @@ - - + + - + 0 0 - - Default + + Advance - - - - - - Appearance + + true + + false + + + + + + Condition + + + + + + + + + + Edit + + + + + + + Validate + + + + + + + + + + + 0 + 0 + + + + Edit + + + + + + + Filter + + + + + + + + + + Edit + + + + - - - - Qt::Horizontal + + + + + + + + + Expression + + + + + + + Appearance + + + + + + + Required + + + + + + + Default + + + + + + + + + + + + + + + + + + 8 - + + 0 + + - - cboDataType - + + + QgsCollapsibleGroupBox + QWidget +
qgscollapsiblegroupbox.h
+
+