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))
Esempio n. 2
0
    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'))
Esempio n. 3
0
    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'))