def __init__(self, iface): """Constructor. :param iface: An interface instance that will be passed to this class which provides the hook by which you can manipulate the QGIS application at run time. :type iface: QgsInterface """ # Save reference to the QGIS interface self.iface = iface # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # initialize locale locale = QLocale(QSettings().value('locale/userLocale')) locale_path = os.path.join(self.plugin_dir, 'i18n') self.translator = QTranslator() self.translator.load(locale, 'QFieldSync', '_', locale_path) QCoreApplication.installTranslator(self.translator) # Declare instance attributes self.actions = [] self.menu = self.tr(u'&QFieldSync') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'QFieldSync') self.toolbar.setObjectName(u'QFieldSync') # instance of the QgsOfflineEditing self.offline_editing = QgsOfflineEditing() self.preferences = Preferences() QgsProject.instance().readProject.connect(self.update_button_enabled_status) # store warnings from last run self.last_action_warnings = []
def test_updateFeatures(self): ol, offline_layer = self._testInit() # Edit feature 2 feat2 = self._getFeatureByAttribute(offline_layer, 'name', "'name 2'") self.assertTrue(offline_layer.startEditing()) self.assertTrue(offline_layer.changeAttributeValue(feat2.id(), offline_layer.fields().lookupField('name'), 'name 2 edited')) self.assertTrue(offline_layer.changeGeometry(feat2.id(), QgsGeometry.fromPointXY(QgsPointXY(33.0, 60.0)))) self.assertTrue(offline_layer.commitChanges()) feat2 = self._getFeatureByAttribute(offline_layer, 'name', "'name 2 edited'") self.assertTrue(ol.isOfflineProject()) # Sync ol.synchronize() sleep(2) # Does anybody know why the sleep is needed? Is that a threaded WFS consequence? online_layer = list(self.registry.mapLayers().values())[0] self.assertTrue(online_layer.isValid()) self.assertFalse(online_layer.name().find('(offline)') > -1) self.assertEqual(len([f for f in online_layer.getFeatures()]), len(TEST_FEATURES)) # Check that data have changed in the backend (raise exception if not found) feat2 = self._getFeatureByAttribute(self._getLayer('test_point'), 'name', "'name 2 edited'") feat2 = self._getFeatureByAttribute(online_layer, 'name', "'name 2 edited'") self.assertEqual(feat2.geometry().asPoint().toString(), QgsPointXY(33.0, 60.0).toString()) # Check that all other features have not changed layer = self._getLayer('test_point') self.assertTrue(self._compareFeature(layer, TEST_FEATURES[1 - 1])) self.assertTrue(self._compareFeature(layer, TEST_FEATURES[3 - 1])) self.assertTrue(self._compareFeature(layer, TEST_FEATURES[4 - 1])) # Test for regression on double sync (it was a SEGFAULT) # goes offline ol = QgsOfflineEditing() offline_layer = list(self.registry.mapLayers().values())[0] # Edit feature 2 feat2 = self._getFeatureByAttribute(offline_layer, 'name', "'name 2 edited'") self.assertTrue(offline_layer.startEditing()) self.assertTrue(offline_layer.changeAttributeValue(feat2.id(), offline_layer.fields().lookupField('name'), 'name 2')) self.assertTrue(offline_layer.changeGeometry(feat2.id(), QgsGeometry.fromPointXY(TEST_FEATURES[1][2]))) # Edit feat 4 feat4 = self._getFeatureByAttribute(offline_layer, 'name', "'name 4'") self.assertTrue(offline_layer.changeAttributeValue(feat4.id(), offline_layer.fields().lookupField('name'), 'name 4 edited')) self.assertTrue(offline_layer.commitChanges()) # Sync ol.synchronize() # Does anybody knows why the sleep is needed? Is that a threaded WFS consequence? sleep(1) online_layer = list(self.registry.mapLayers().values())[0] layer = self._getLayer('test_point') # Check that data have changed in the backend (raise exception if not found) feat4 = self._getFeatureByAttribute(layer, 'name', "'name 4 edited'") feat4 = self._getFeatureByAttribute(online_layer, 'name', "'name 4 edited'") feat2 = self._getFeatureByAttribute(layer, 'name', "'name 2'") feat2 = self._getFeatureByAttribute(online_layer, 'name', "'name 2'") # Check that all other features have not changed layer = self._getLayer('test_point') self.assertTrue(self._compareFeature(layer, TEST_FEATURES[1 - 1])) self.assertTrue(self._compareFeature(layer, TEST_FEATURES[2 - 1])) self.assertTrue(self._compareFeature(layer, TEST_FEATURES[3 - 1]))
def test_primary_keys_custom_property(self): source_folder = tempfile.mkdtemp() export_folder = tempfile.mkdtemp() shutil.copytree(os.path.join(test_data_folder(), 'pk_project'), os.path.join(source_folder, 'pk_project')) project = self.load_project( os.path.join(source_folder, 'pk_project', 'project.qgs')) extent = QgsRectangle() offline_editing = QgsOfflineEditing() offline_converter = OfflineConverter(project, export_folder, extent, offline_editing) offline_converter.convert() exported_project = self.load_project( os.path.join(export_folder, 'project_qfield.qgs')) if Qgis.QGIS_VERSION_INT < 31601: layer = exported_project.mapLayersByName('somedata (offline)')[0] else: layer = exported_project.mapLayersByName('somedata')[0] self.assertEqual( layer.customProperty('QFieldSync/sourceDataPrimaryKeys'), 'pk') shutil.rmtree(export_folder) shutil.rmtree(source_folder)
def test_copy(self): source_folder = tempfile.mkdtemp() export_folder = tempfile.mkdtemp() shutil.copytree(os.path.join(test_data_folder(), 'simple_project'), os.path.join(source_folder, 'simple_project')) project = self.load_project( os.path.join(source_folder, 'simple_project', 'project.qgs')) extent = QgsRectangle() offline_editing = QgsOfflineEditing() offline_converter = OfflineConverter(project, export_folder, extent, offline_editing) offline_converter.convert() files = os.listdir(export_folder) self.assertIn('project_qfield.qgs', files) self.assertIn('france_parts_shape.shp', files) self.assertIn('france_parts_shape.dbf', files) self.assertIn('curved_polys.gpkg', files) self.assertIn('spatialite.db', files) dcim_folder = os.path.join(export_folder, "DCIM") dcim_files = os.listdir(dcim_folder) self.assertIn('qfield-photo_1.jpg', dcim_files) self.assertIn('qfield-photo_2.jpg', dcim_files) self.assertIn('qfield-photo_3.jpg', dcim_files) dcim_subfolder = os.path.join(dcim_folder, "subfolder") dcim_subfiles = os.listdir(dcim_subfolder) self.assertIn('qfield-photo_sub_1.jpg', dcim_subfiles) self.assertIn('qfield-photo_sub_2.jpg', dcim_subfiles) self.assertIn('qfield-photo_sub_3.jpg', dcim_subfiles) shutil.rmtree(export_folder) shutil.rmtree(source_folder)
def test_open_dialog(self): preferences = Preferences() offline_editing = QgsOfflineEditing() dlg = PackageDialog(self.iface, preferences, QgsProject.instance(), offline_editing) dlg.show()
def _testInit(self): """ Preliminary checks for each test """ # goes offline ol = QgsOfflineEditing() online_layer = list(self.registry.mapLayers().values())[0] self.assertTrue(online_layer.isSpatial()) # Check we have features self.assertEqual(len([f for f in online_layer.getFeatures()]), len(TEST_FEATURES)) self.assertTrue(ol.convertToOfflineProject(self.temp_path, 'offlineDbFile.sqlite', [online_layer.id()])) offline_layer = list(self.registry.mapLayers().values())[0] self.assertTrue(offline_layer.isSpatial()) self.assertTrue(offline_layer.isValid()) self.assertTrue(offline_layer.name().find('(offline)') > -1) self.assertEqual(len([f for f in offline_layer.getFeatures()]), len(TEST_FEATURES)) return ol, offline_layer
class QFieldSync(object): """QGIS Plugin Implementation.""" QFIELD_SCOPE = "QFieldSync" push_dlg = None def __init__(self, iface): """Constructor. :param iface: An interface instance that will be passed to this class which provides the hook by which you can manipulate the QGIS application at run time. :type iface: QgsInterface """ # Save reference to the QGIS interface self.iface = iface # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # initialize locale locale_str = QSettings().value('locale/userLocale') if isinstance(locale_str, str): locale = QLocale(locale_str) else: locale = QLocale() locale_path = os.path.join(self.plugin_dir, 'i18n') self.translator = QTranslator() self.translator.load(locale, 'qfieldsync', '_', locale_path) QCoreApplication.installTranslator(self.translator) # Declare instance attributes self.actions = [] self.menu = self.tr('&QFieldSync') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar('QFieldSync') self.toolbar.setObjectName('QFieldSync') # instance of the map config widget factory, shown in layer properties self.mapLayerConfigWidgetFactory = MapLayerConfigWidgetFactory( 'QField', QIcon( os.path.join(os.path.dirname(__file__), 'resources/qfield_logo.svg'))) # instance of the QgsOfflineEditing self.offline_editing = QgsOfflineEditing() self.preferences = Preferences() QgsProject.instance().readProject.connect( self.update_button_enabled_status) # store warnings from last run self.last_action_warnings = [] # noinspection PyMethodMayBeStatic def tr(self, message): """Get the translation for a string using Qt translation API. We implement this ourselves since we do not inherit QObject. :param message: String for translation. :type message: str, QString :returns: Translated version of message. :rtype: QString """ # noinspection PyTypeChecker,PyArgumentList,PyCallByClass return QCoreApplication.translate('QFieldSync', message) def add_action(self, icon_path, text, callback, enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, parent=None): """Add a toolbar icon to the toolbar. :param icon_path: Path to the icon for this action. Can be a resource path (e.g. ':/plugins/foo/bar.png') or a normal file system path. :type icon_path: str :param text: Text that should be shown in menu items for this action. :type text: str :param callback: Function to be called when the action is triggered. :type callback: function :param enabled_flag: A flag indicating if the action should be enabled by default. Defaults to True. :type enabled_flag: bool :param add_to_menu: Flag indicating whether the action should also be added to the menu. Defaults to True. :type add_to_menu: bool :param add_to_toolbar: Flag indicating whether the action should also be added to the toolbar. Defaults to True. :type add_to_toolbar: bool :param status_tip: Optional text to show in a popup when mouse pointer hovers over the action. :type status_tip: str :param parent: Parent widget for the new action. Defaults None. :type parent: QWidget :param whats_this: Optional text to show in the status bar when the mouse pointer hovers over the action. :returns: The action that was created. Note that the action is also added to self.actions list. :rtype: QAction """ icon = QIcon(icon_path) action = QAction(icon, text, parent) action.triggered.connect(callback) action.setEnabled(enabled_flag) if status_tip is not None: action.setStatusTip(status_tip) if whats_this is not None: action.setWhatsThis(whats_this) if add_to_toolbar: self.toolbar.addAction(action) if add_to_menu: self.iface.addPluginToMenu(self.menu, action) self.actions.append(action) return action def initGui(self): """Create the menu entries and toolbar icons inside the QGIS GUI.""" self.push_action = self.add_action(QIcon( os.path.join(os.path.dirname(__file__), 'resources/package.svg')), text=self.tr('Package for QField'), callback=self.show_package_dialog, parent=self.iface.mainWindow()) self.add_action(QIcon( os.path.join(os.path.dirname(__file__), 'resources/synchronize.svg')), text=self.tr('Synchronize from QField'), callback=self.show_synchronize_dialog, parent=self.iface.mainWindow()) self.add_action( QIcon( os.path.join(os.path.dirname(__file__), './resources/project_properties.svg')), text=self.tr('Configure Current Project'), callback=self.show_project_configuration_dialog, parent=self.iface.mainWindow(), ) actions = self.iface.pluginMenu().actions() for action in actions: if action.text() == self.menu: action.menu().addSeparator() self.add_action(QgsApplication.getThemeIcon("/mActionOptions.svg"), text=self.tr('Preferences'), callback=self.show_preferences_dialog, parent=self.iface.mainWindow(), add_to_toolbar=False) self.iface.registerMapLayerConfigWidgetFactory( self.mapLayerConfigWidgetFactory) if Qgis.QGIS_VERSION_INT >= 31500: self.project_properties_factory = QFieldSyncProjectPropertiesFactory( ) self.project_properties_factory.setTitle('QField') self.iface.registerProjectPropertiesWidgetFactory( self.project_properties_factory) self.options_factory = QFieldSyncOptionsFactory() self.options_factory.setTitle(self.tr('QField')) self.iface.registerOptionsWidgetFactory(self.options_factory) self.update_button_enabled_status() def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" for action in self.actions: self.iface.removePluginMenu(self.tr('&QFieldSync'), action) self.iface.removeToolBarIcon(action) # remove the toolbar del self.toolbar self.iface.unregisterMapLayerConfigWidgetFactory( self.mapLayerConfigWidgetFactory) if Qgis.QGIS_VERSION_INT >= 31500: self.iface.unregisterProjectPropertiesWidgetFactory( self.project_properties_factory) self.iface.unregisterOptionsWidgetFactory(self.options_factory) def show_preferences_dialog(self): self.iface.showOptionsDialog(self.iface.mainWindow(), currentPage='QFieldPreferences') def show_synchronize_dialog(self): """ Synchronize from QField """ dlg = SynchronizeDialog(self.iface, self.offline_editing, self.iface.mainWindow()) dlg.exec_() def show_package_dialog(self): """ Push to QField """ self.push_dlg = PackageDialog(self.iface, QgsProject.instance(), self.offline_editing, self.iface.mainWindow()) self.push_dlg.setAttribute(Qt.WA_DeleteOnClose) self.push_dlg.setWindowFlags(self.push_dlg.windowFlags() | Qt.Tool) self.push_dlg.show() self.push_dlg.finished.connect(self.push_dialog_finished) self.update_button_enabled_status() def show_project_configuration_dialog(self): """ Show the project configuration dialog. """ if Qgis.QGIS_VERSION_INT >= 31500: self.iface.showProjectPropertiesDialog('QField') else: dlg = ProjectConfigurationDialog(self.iface.mainWindow()) dlg.exec_() def action_start(self): self.clear_last_action_warnings() def clear_last_action_warnings(self): self.last_action_warnings = [] def push_dialog_finished(self): """ When the push dialog is closed, make sure it's no longer enabled before entering update_button_enabled_status() """ try: self.push_dlg.setEnabled(False) except RuntimeError: pass self.update_button_enabled_status() def update_button_enabled_status(self): """ Will update the plugin buttons according to open dialog and project properties. """ try: dialog_is_enabled = self.push_dlg and self.push_dlg.isEnabled() except RuntimeError: dialog_is_enabled = False if self.offline_editing.isOfflineProject() or dialog_is_enabled: self.push_action.setEnabled(False) else: self.push_action.setEnabled(True)
def test_open_dialog(self): offline_editing = QgsOfflineEditing() dlg = SynchronizeDialog(self.iface, offline_editing) dlg.show()
class QFieldSync(object): """QGIS Plugin Implementation.""" QFIELD_SCOPE = "QFieldSync" push_dlg = None def __init__(self, iface): """Constructor. :param iface: An interface instance that will be passed to this class which provides the hook by which you can manipulate the QGIS application at run time. :type iface: QgsInterface """ # Save reference to the QGIS interface self.iface = iface # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # initialize locale locale = QLocale(QSettings().value('locale/userLocale')) locale_path = os.path.join(self.plugin_dir, 'i18n') self.translator = QTranslator() self.translator.load(locale, 'QFieldSync', '_', locale_path) QCoreApplication.installTranslator(self.translator) # Declare instance attributes self.actions = [] self.menu = self.tr(u'&QFieldSync') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'QFieldSync') self.toolbar.setObjectName(u'QFieldSync') # instance of the QgsOfflineEditing self.offline_editing = QgsOfflineEditing() self.preferences = Preferences() QgsProject.instance().readProject.connect(self.update_button_enabled_status) # store warnings from last run self.last_action_warnings = [] # noinspection PyMethodMayBeStatic def tr(self, message): """Get the translation for a string using Qt translation API. We implement this ourselves since we do not inherit QObject. :param message: String for translation. :type message: str, QString :returns: Translated version of message. :rtype: QString """ # noinspection PyTypeChecker,PyArgumentList,PyCallByClass return QCoreApplication.translate('QFieldSync', message) def add_action( self, icon_path, text, callback, enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, parent=None): """Add a toolbar icon to the toolbar. :param icon_path: Path to the icon for this action. Can be a resource path (e.g. ':/plugins/foo/bar.png') or a normal file system path. :type icon_path: str :param text: Text that should be shown in menu items for this action. :type text: str :param callback: Function to be called when the action is triggered. :type callback: function :param enabled_flag: A flag indicating if the action should be enabled by default. Defaults to True. :type enabled_flag: bool :param add_to_menu: Flag indicating whether the action should also be added to the menu. Defaults to True. :type add_to_menu: bool :param add_to_toolbar: Flag indicating whether the action should also be added to the toolbar. Defaults to True. :type add_to_toolbar: bool :param status_tip: Optional text to show in a popup when mouse pointer hovers over the action. :type status_tip: str :param parent: Parent widget for the new action. Defaults None. :type parent: QWidget :param whats_this: Optional text to show in the status bar when the mouse pointer hovers over the action. :returns: The action that was created. Note that the action is also added to self.actions list. :rtype: QAction """ icon = QIcon(icon_path) action = QAction(icon, text, parent) action.triggered.connect(callback) action.setEnabled(enabled_flag) if status_tip is not None: action.setStatusTip(status_tip) if whats_this is not None: action.setWhatsThis(whats_this) if add_to_toolbar: self.toolbar.addAction(action) if add_to_menu: self.iface.addPluginToMenu( self.menu, action) self.actions.append(action) return action def initGui(self): """Create the menu entries and toolbar icons inside the QGIS GUI.""" self.push_action = self.add_action( ':/plugins/qfieldsync/refresh.png', text=self.tr(u'Package for QField'), callback=self.show_package_dialog, parent=self.iface.mainWindow()) self.add_action( ':/plugins/qfieldsync/refresh-reverse.png', text=self.tr(u'Synchronize from QField'), callback=self.show_synchronize_dialog, parent=self.iface.mainWindow()) self.add_action( ':/plugins/qfieldsync/icon.png', text=self.tr(u'Project Configuration'), callback=self.show_project_configuration_dialog, parent=self.iface.mainWindow(), add_to_toolbar=False ) self.add_action( ':/plugins/qfieldsync/icon.png', text=self.tr(u'Preferences'), callback=self.show_preferences_dialog, parent=self.iface.mainWindow(), add_to_toolbar=False) self.update_button_enabled_status() def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" for action in self.actions: self.iface.removePluginMenu( self.tr(u'&QFieldSync'), action) self.iface.removeToolBarIcon(action) # remove the toolbar del self.toolbar def show_preferences_dialog(self): dlg = PreferencesDialog(self.preferences, self.iface.mainWindow()) dlg.exec_() def show_synchronize_dialog(self): """ Synchronize from QField """ dlg = SynchronizeDialog(self.iface, self.preferences, self.offline_editing, self.iface.mainWindow()) dlg.exec_() def show_package_dialog(self): """ Push to QField """ self.push_dlg = PackageDialog(self.iface, self.preferences, QgsProject.instance(), self.offline_editing, self.iface.mainWindow()) self.push_dlg.setAttribute(Qt.WA_DeleteOnClose) self.push_dlg.setWindowFlags(self.push_dlg.windowFlags() | Qt.Tool) self.push_dlg.show() self.push_dlg.finished.connect(self.push_dialog_finished) self.update_button_enabled_status() def show_project_configuration_dialog(self): """ Show the project configuration dialog. """ dlg = ProjectConfigurationDialog(self.iface, self.iface.mainWindow()) dlg.exec_() def action_start(self): self.clear_last_action_warnings() def clear_last_action_warnings(self): self.last_action_warnings = [] def push_dialog_finished(self): """ When the push dialog is closed, make sure it's no longer enabled before entering update_button_enabled_status() """ try: self.push_dlg.setEnabled(False) except RuntimeError: pass self.update_button_enabled_status() def update_button_enabled_status(self): """ Will update the plugin buttons according to open dialog and project properties. """ try: dialog_is_enabled = self.push_dlg and self.push_dlg.isEnabled() except RuntimeError: dialog_is_enabled = False if self.offline_editing.isOfflineProject() or dialog_is_enabled: self.push_action.setEnabled(False) else: self.push_action.setEnabled(True)
def test_open_dialog(self): preferences = Preferences() offline_editing = QgsOfflineEditing() dlg = ProjectConfigurationDialog(self.iface) dlg.show()
def test_open_dialog(self): offline_editing = QgsOfflineEditing() dlg = ProjectConfigurationDialog() dlg.show()
def isNotTemp(layer): return not layer.isTemporary() # QgsApplication.setPrefixPath("/path/to/qgis/installation", True) qgs = QgsApplication([], False) qgs.initQgis() project = QgsProject.instance() project.read(SOURCE_PROJECT_PATH) project.write(OFFLINE_PROJECT_PATH) layers = [l for l in project.mapLayers().values()] vectorLayers = filter(isVector, layers) notTempLayers = filter(isNotTemp, vectorLayers) ids = [l.id() for l in notTempLayers] # print(ids) offlineEditing = QgsOfflineEditing() offlineEditing.convertToOfflineProject(OFFLINE_DATA_PATH, OFFLINE_DB_FILE, ids, False, QgsOfflineEditing.GPKG) qgs.exitQgis()
def convert_to_offline(self, db, surveyor_expression_dict, export_dir): sys.path.append(PLUGINS_DIR) from qfieldsync.core.layer import LayerSource, SyncAction from qfieldsync.core.offline_converter import OfflineConverter from qfieldsync.core.project import ProjectConfiguration project = QgsProject.instance() extent = QgsRectangle() offline_editing = QgsOfflineEditing() # Configure project project_configuration = ProjectConfiguration(project) project_configuration.create_base_map = False project_configuration.offline_copy_only_aoi = False project_configuration.use_layer_selection = True # Layer config layer_sync_action = LayerConfig.get_field_data_capture_layer_config( db.names) total_projects = len(surveyor_expression_dict) current_progress = 0 for surveyor, layer_config in surveyor_expression_dict.items(): export_folder = os.path.join(export_dir, surveyor) # Get layers (cannot be done out of this for loop because the project is closed and layers are deleted) layers = { layer_name: None for layer_name, _ in layer_sync_action.items() } self.app.core.get_layers(db, layers, True) if not layers: return False, QCoreApplication.translate( "FieldDataCapture", "At least one layer could not be found.") # Configure layers for layer_name, layer in layers.items(): layer_source = LayerSource(layer) layer_source.action = layer_sync_action[layer_name] if layer_name in layer_config: layer_source.select_expression = layer_config[layer_name] layer_source.apply() offline_converter = OfflineConverter(project, export_folder, extent, offline_editing) offline_converter.convert() offline_editing.layerProgressUpdated.disconnect( offline_converter.on_offline_editing_next_layer) offline_editing.progressModeSet.disconnect( offline_converter.on_offline_editing_max_changed) offline_editing.progressUpdated.disconnect( offline_converter.offline_editing_task_progress) current_progress += 1 self.total_progress_updated.emit( int(100 * current_progress / total_projects)) return True, QCoreApplication.translate( "FieldDataCapture", "{count} offline projects have been successfully created in <a href='file:///{normalized_path}'>{path}</a>!" ).format(count=total_projects, normalized_path=normalize_local_url(export_dir), path=export_dir)