def start_synchronization(self): self.button_box.button(QDialogButtonBox.Save).setEnabled(False) qfield_folder = self.qfieldDir.text() self.preferences.set_value('importDirectoryProject', qfield_folder) try: current_import_file_checksum = import_file_checksum(qfield_folder) imported_files_checksums = import_checksums_of_project( qfield_folder) if imported_files_checksums and current_import_file_checksum and current_import_file_checksum in imported_files_checksums: message = self.tr( "Data from this file are already synchronized with the original project." ) raise NoProjectFoundError(message) qgs_file = get_project_in_folder(qfield_folder) open_project(qgs_file) self.offline_editing.progressStopped.connect(self.update_done) self.offline_editing.layerProgressUpdated.connect( self.update_total) self.offline_editing.progressModeSet.connect(self.update_mode) self.offline_editing.progressUpdated.connect(self.update_value) self.offline_editing.synchronize() if self.offline_editing_done: original_project_path = ProjectConfiguration( QgsProject.instance()).original_project_path if original_project_path: # import the DCIM folder copy_images( os.path.join(qfield_folder, "DCIM"), os.path.join(os.path.dirname(original_project_path), "DCIM")) if open_project(original_project_path): # save the data_file_checksum to the project and save it imported_files_checksums.append( import_file_checksum(qfield_folder)) ProjectConfiguration(QgsProject.instance( )).imported_files_checksums = imported_files_checksums QgsProject.instance().write() self.iface.messageBar().pushInfo( 'QFieldSync', self.tr("Opened original project {}".format( original_project_path))) else: self.iface.messageBar().pushInfo( 'QFieldSync', self. tr("The data has been synchronized successfully but the original project ({}) could not be opened" .format(original_project_path))) else: self.iface.messageBar().pushInfo( 'QFieldSync', self.tr("No original project path found")) self.close() else: message = self.tr( "The project you imported does not seem to be an offline project" ) raise NoProjectFoundError(message) except NoProjectFoundError as e: self.iface.messageBar().pushWarning('QFieldSync', str(e))
def convert(self): """ Convert the project to a portable project. :param offline_editing: The offline editing instance :param export_folder: The folder to export to """ project = QgsProject.instance() original_project = project original_project_path = project.fileName() project_filename, _ = os.path.splitext( os.path.basename(original_project_path)) # Write a backup of the current project to a temporary file project_backup_folder = tempfile.mkdtemp() backup_project_path = os.path.join(project_backup_folder, project_filename + '.qgs') QgsProject.instance().write(backup_project_path) try: if not os.path.exists(self.export_folder): os.makedirs(self.export_folder) QApplication.setOverrideCursor(Qt.WaitCursor) self.__offline_layers = list() self.__layers = list(project.mapLayers().values()) original_layer_info = {} for layer in self.__layers: original_layer_info[layer.id()] = (layer.source(), layer.name()) # We store the pks of the original vector layers # and we check that the primary key fields names don't # have a comma in the name original_pk_fields_by_layer_name = {} for layer in self.__layers: if layer.type() == QgsMapLayer.VectorLayer: keys = [] for idx in layer.primaryKeyAttributes(): key = layer.fields()[idx].name() assert (',' not in key), 'Comma in field names not allowed' keys.append(key) original_pk_fields_by_layer_name[layer.name()] = ','.join( keys) self.total_progress_updated.emit(0, 1, self.trUtf8('Creating base map…')) # Create the base map before layers are removed if self.project_configuration.create_base_map: if 'processing' not in qgis.utils.plugins: QMessageBox.warning( None, self.tr('QFieldSync requires processing'), self. tr('Creating a basemap with QFieldSync requires the processing plugin to be enabled. Processing is not enabled on your system. Please go to Plugins > Manage and Install Plugins and enable processing.' )) return if self.project_configuration.base_map_type == ProjectProperties.BaseMapType.SINGLE_LAYER: self.createBaseMapLayer( None, self.project_configuration.base_map_layer, self.project_configuration.base_map_tile_size, self.project_configuration.base_map_mupp) else: self.createBaseMapLayer( self.project_configuration.base_map_theme, None, self.project_configuration.base_map_tile_size, self.project_configuration.base_map_mupp) # Loop through all layers and copy/remove/offline them pathResolver = QgsProject.instance().pathResolver() copied_files = list() for current_layer_index, layer in enumerate(self.__layers): self.total_progress_updated.emit( current_layer_index - len(self.__offline_layers), len(self.__layers), self.trUtf8('Copying layers…')) layer_source = LayerSource(layer) if not layer_source.is_supported: project.removeMapLayer(layer) continue if layer.dataProvider() is not None: md = QgsProviderRegistry.instance().providerMetadata( layer.dataProvider().name()) if md is not None: decoded = md.decodeUri(layer.source()) if "path" in decoded: path = pathResolver.writePath(decoded["path"]) if path.startswith("localized:"): # Layer stored in localized data path, skip continue if layer_source.action == SyncAction.OFFLINE: if self.project_configuration.offline_copy_only_aoi and not self.project_configuration.offline_copy_only_selected_features: layer.selectByRect(self.extent) elif self.project_configuration.offline_copy_only_aoi and self.project_configuration.offline_copy_only_selected_features: # This option is only possible via API QgsApplication.instance().messageLog().logMessage( self. tr('Both "Area of Interest" and "only selected features" options were enabled, tha latter takes precedence.' ), 'QFieldSync') self.__offline_layers.append(layer) # Store the primary key field name(s) as comma separated custom property if layer.type() == QgsMapLayer.VectorLayer: key_fields = ','.join([ layer.fields()[x].name() for x in layer.primaryKeyAttributes() ]) layer.setCustomProperty( 'QFieldSync/sourceDataPrimaryKeys', key_fields) elif layer_source.action == SyncAction.NO_ACTION: copied_files = layer_source.copy(self.export_folder, copied_files) elif layer_source.action == SyncAction.KEEP_EXISTENT: layer_source.copy(self.export_folder, copied_files, True) elif layer_source.action == SyncAction.REMOVE: project.removeMapLayer(layer) project_path = os.path.join(self.export_folder, project_filename + "_qfield.qgs") # save the original project path ProjectConfiguration( project).original_project_path = original_project_path # save the offline project twice so that the offline plugin can "know" that it's a relative path QgsProject.instance().write(project_path) # export the DCIM folder copy_images( os.path.join(os.path.dirname(original_project_path), "DCIM"), os.path.join(os.path.dirname(project_path), "DCIM")) try: # Run the offline plugin for gpkg gpkg_filename = "data.gpkg" if self.__offline_layers: offline_layer_ids = [l.id() for l in self.__offline_layers] only_selected = self.project_configuration.offline_copy_only_aoi or self.project_configuration.offline_copy_only_selected_features if not self.offline_editing.convertToOfflineProject( self.export_folder, gpkg_filename, offline_layer_ids, only_selected, self.offline_editing.GPKG): raise Exception( self. tr("Error trying to convert layers to offline layers" )) except AttributeError: # Run the offline plugin for spatialite spatialite_filename = "data.sqlite" if self.__offline_layers: offline_layer_ids = [l.id() for l in self.__offline_layers] only_selected = self.project_configuration.offline_copy_only_aoi or self.project_configuration.offline_copy_only_selected_features if not self.offline_editing.convertToOfflineProject( self.export_folder, spatialite_filename, offline_layer_ids, only_selected): raise Exception( self. tr("Error trying to convert layers to offline layers" )) # Disable project options that could create problems on a portable # project with offline layers if self.__offline_layers: QgsProject.instance().setEvaluateDefaultValues(False) QgsProject.instance().setAutoTransaction(False) # check if value relations point to offline layers and adjust if necessary for layer in project.mapLayers().values(): if layer.type() == QgsMapLayer.VectorLayer: # Before QGIS 3.14 the custom properties of a layer are not # kept into the new layer during the conversion to offline project # So we try to identify the new created layer by its name and # we set the custom properties again. if not layer.customProperty( 'QFieldSync/cloudPrimaryKeys'): original_layer_name = layer.name().rsplit(' ', 1)[0] stored_fields = original_pk_fields_by_layer_name.get( original_layer_name, None) if stored_fields: layer.setCustomProperty( 'QFieldSync/sourceDataPrimaryKeys', stored_fields) for field in layer.fields(): ews = field.editorWidgetSetup() if ews.type() == 'ValueRelation': widget_config = ews.config() online_layer_id = widget_config['Layer'] if project.mapLayer(online_layer_id): continue layer_id = None loose_layer_id = None for offline_layer in project.mapLayers( ).values(): if offline_layer.customProperty( 'remoteSource' ) == original_layer_info[online_layer_id][ 0]: # First try strict matching: the offline layer should have a "remoteSource" property layer_id = offline_layer.id() break elif offline_layer.name().startswith( original_layer_info[ online_layer_id][1] + ' '): # If that did not work, go with loose matching # The offline layer should start with the online layer name + a translated version of " (offline)" loose_layer_id = offline_layer.id() widget_config[ 'Layer'] = layer_id or loose_layer_id offline_ews = QgsEditorWidgetSetup( ews.type(), widget_config) layer.setEditorWidgetSetup( layer.fields().indexOf(field.name()), offline_ews) # Now we have a project state which can be saved as offline project QgsProject.instance().write(project_path) finally: # We need to let the app handle events before loading the next project or QGIS will crash with rasters QCoreApplication.processEvents() QgsProject.instance().clear() QCoreApplication.processEvents() QgsProject.instance().read(backup_project_path) QgsProject.instance().setFileName(original_project_path) QApplication.restoreOverrideCursor() self.offline_editing.layerProgressUpdated.disconnect( self.on_offline_editing_next_layer) self.offline_editing.progressModeSet.disconnect( self.on_offline_editing_max_changed) self.offline_editing.progressUpdated.disconnect( self.offline_editing_task_progress) self.total_progress_updated.emit(100, 100, self.tr('Finished'))
def convert(self): """ Convert the project to a portable project. :param offline_editing: The offline editing instance :param export_folder: The folder to export to """ project = QgsProject.instance() original_project = project original_project_path = project.fileName() project_filename, _ = os.path.splitext( os.path.basename(original_project_path)) # Write a backup of the current project to a temporary file project_backup_folder = tempfile.mkdtemp() backup_project_path = os.path.join(project_backup_folder, project_filename + '.qgs') QgsProject.instance().write(backup_project_path) try: if not os.path.exists(self.export_folder): os.makedirs(self.export_folder) QApplication.setOverrideCursor(Qt.WaitCursor) self.__offline_layers = list() self.__layers = list(project.mapLayers().values()) original_layer_info = {} for layer in self.__layers: original_layer_info[layer.id()] = (layer.source(), layer.name()) self.total_progress_updated.emit(0, 1, self.trUtf8('Creating base map…')) # Create the base map before layers are removed if self.project_configuration.create_base_map: if 'processing' not in qgis.utils.plugins: QMessageBox.warning( None, self.tr('QFieldSync requires processing'), self. tr('Creating a basemap with QFieldSync requires the processing plugin to be enabled. Processing is not enabled on your system. Please go to Plugins > Manage and Install Plugins and enable processing.' )) return if self.project_configuration.base_map_type == ProjectProperties.BaseMapType.SINGLE_LAYER: self.createBaseMapLayer( None, self.project_configuration.base_map_layer, self.project_configuration.base_map_tile_size, self.project_configuration.base_map_mupp) else: self.createBaseMapLayer( self.project_configuration.base_map_theme, None, self.project_configuration.base_map_tile_size, self.project_configuration.base_map_mupp) # Loop through all layers and copy/remove/offline them copied_files = list() for current_layer_index, layer in enumerate(self.__layers): self.total_progress_updated.emit( current_layer_index - len(self.__offline_layers), len(self.__layers), self.trUtf8('Copying layers…')) layer_source = LayerSource(layer) if layer_source.action == SyncAction.OFFLINE: if self.project_configuration.offline_copy_only_aoi: layer.selectByRect(self.extent) self.__offline_layers.append(layer) elif layer_source.action == SyncAction.NO_ACTION: copied_files = layer_source.copy(self.export_folder, copied_files) elif layer_source.action == SyncAction.KEEP_EXISTENT: layer_source.copy(self.export_folder, copied_files, True) elif layer_source.action == SyncAction.REMOVE: project.removeMapLayer(layer) project_path = os.path.join(self.export_folder, project_filename + "_qfield.qgs") # save the original project path ProjectConfiguration( project).original_project_path = original_project_path # save the offline project twice so that the offline plugin can "know" that it's a relative path QgsProject.instance().write(project_path) # export the DCIM folder copy_images( os.path.join(os.path.dirname(original_project_path), "DCIM"), os.path.join(os.path.dirname(project_path), "DCIM")) try: # Run the offline plugin for gpkg gpkg_filename = "data.gpkg" if self.__offline_layers: offline_layer_ids = [l.id() for l in self.__offline_layers] if not self.offline_editing.convertToOfflineProject( self.export_folder, gpkg_filename, offline_layer_ids, self.project_configuration.offline_copy_only_aoi, self.offline_editing.GPKG): raise Exception( self. tr("Error trying to convert layers to offline layers" )) except AttributeError: # Run the offline plugin for spatialite spatialite_filename = "data.sqlite" if self.__offline_layers: offline_layer_ids = [l.id() for l in self.__offline_layers] if not self.offline_editing.convertToOfflineProject( self.export_folder, spatialite_filename, offline_layer_ids, self.project_configuration.offline_copy_only_aoi): raise Exception( self. tr("Error trying to convert layers to offline layers" )) # Disable project options that could create problems on a portable # project with offline layers if self.__offline_layers: QgsProject.instance().setEvaluateDefaultValues(False) QgsProject.instance().setAutoTransaction(False) # check if value relations point to offline layers and adjust if necessary for layer in project.mapLayers().values(): if layer.type() == QgsMapLayer.VectorLayer: for field in layer.fields(): ews = field.editorWidgetSetup() if ews.type() == 'ValueRelation': widget_config = ews.config() online_layer_id = widget_config['Layer'] if project.mapLayer(online_layer_id): continue layer_id = None loose_layer_id = None for offline_layer in project.mapLayers( ).values(): if offline_layer.customProperty( 'remoteSource' ) == original_layer_info[online_layer_id][ 0]: # First try strict matching: the offline layer should have a "remoteSource" property layer_id = offline_layer.id() break elif offline_layer.name().startswith( original_layer_info[ online_layer_id][1] + ' '): # If that did not work, go with loose matching # The offline layer should start with the online layer name + a translated version of " (offline)" loose_layer_id = offline_layer.id() widget_config[ 'Layer'] = layer_id or loose_layer_id offline_ews = QgsEditorWidgetSetup( ews.type(), widget_config) layer.setEditorWidgetSetup( layer.fields().indexOf(field.name()), offline_ews) # Now we have a project state which can be saved as offline project QgsProject.instance().write(project_path) finally: # We need to let the app handle events before loading the next project or QGIS will crash with rasters QCoreApplication.processEvents() QgsProject.instance().clear() QCoreApplication.processEvents() QgsProject.instance().read(backup_project_path) QgsProject.instance().setFileName(original_project_path) QApplication.restoreOverrideCursor() self.total_progress_updated.emit(100, 100, self.tr('Finished'))