diff --git a/qfieldsync/core/cloud_converter.py b/qfieldsync/core/cloud_converter.py deleted file mode 100644 index 1bc577ec..00000000 --- a/qfieldsync/core/cloud_converter.py +++ /dev/null @@ -1,167 +0,0 @@ -""" -/*************************************************************************** - QFieldSync - ------------------- - begin : 2021-07-20 - git sha : $Format:%H$ - copyright : (C) 2021 by OPENGIS.ch - email : info@opengis.ch - ***************************************************************************/ - -/*************************************************************************** - * * - * 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 pathlib import Path - -from libqfieldsync.layer import LayerSource -from libqfieldsync.utils.file_utils import copy_attachments -from libqfieldsync.utils.qgis import get_qgis_files_within_dir, make_temp_qgis_file -from qgis.core import QgsMapLayer, QgsProject, QgsVirtualLayerDefinition -from qgis.PyQt.QtCore import QCoreApplication, QObject, QUrl, pyqtSignal -from qgis.utils import iface - -from qfieldsync.core.errors import QFieldSyncError -from qfieldsync.core.preferences import Preferences -from qfieldsync.utils.qgis_utils import open_project - - -class CloudConverter(QObject): - warning = pyqtSignal(str, str) - total_progress_updated = pyqtSignal(int, int, str) - - def __init__( - self, - project: QgsProject, - export_dirname: str, - ): - super().__init__(parent=None) - self.project = project - self.__layers = [] - - # elipsis workaround - self.trUtf8 = self.tr - - self.export_dirname = Path(export_dirname) - - def convert(self) -> None: # noqa: PLR0912, PLR0915 - """Convert the project to a cloud project.""" - original_project_path = self.project.fileName() - project_path = self.export_dirname.joinpath( - f"{self.project.baseName()}_cloud.qgs" - ) - backup_project_path = make_temp_qgis_file(self.project) - is_converted = False - - try: - if not self.export_dirname.exists(): - self.export_dirname.mkdir(parents=True, exist_ok=True) - - if get_qgis_files_within_dir(self.export_dirname): - raise QFieldSyncError( - self.tr("The destination folder already contains a project file") - ) - - self.total_progress_updated.emit(0, 100, self.trUtf8("Converting project…")) - self.__layers = list(self.project.mapLayers().values()) - - # Loop through all layers and copy them to the destination folder - for current_layer_index, layer in enumerate(self.__layers): - self.total_progress_updated.emit( - current_layer_index, - len(self.__layers), - self.trUtf8("Copying layers…"), - ) - - layer_source = LayerSource(layer) - if not layer_source.is_supported: - self.project.removeMapLayer(layer) - continue - - if layer.dataProvider() is not None: - # layer stored in shared dataset path, skip - if layer_source.is_localized_path: - continue - - if layer.type() == QgsMapLayer.LayerType.VectorLayer: - if ( - layer.dataProvider() - and layer.dataProvider().name() == "virtual" - ): - url = QUrl.fromEncoded(layer.source().encode("ascii")) - valid = url.isValid() - if valid: - definition = QgsVirtualLayerDefinition.fromUrl(url) - for source in definition.sourceLayers(): - if not source.isReferenced(): - valid = False - break - - if not valid: - # virtual layers with non-referenced sources are not supported - self.warning.emit( - self.tr("Cloud Converter"), - self.tr( - "The virtual layer '{}' is not valid or contains non-referenced source(s) and could not be converted and was therefore removed from the cloud project." - ).format(layer.name()), - ) - self.project.removeMapLayer(layer) - continue - else: - if not layer_source.convert_to_gpkg(self.export_dirname): - # something went wrong, remove layer and inform the user that layer will be missing - self.warning.emit( - self.tr("Cloud Converter"), - self.tr( - "The layer '{}' could not be converted and was therefore removed from the cloud project." - ).format(layer.name()), - ) - self.project.removeMapLayer(layer) - continue - else: - layer_source.copy(self.export_dirname, []) - - layer.setCustomProperty( - "QFieldSync/cloud_action", layer_source.default_cloud_action - ) - - # save the offline project twice so that the offline plugin can "know" that it's a relative path - if not self.project.write(str(project_path)): - raise QFieldSyncError( - self.tr('Failed to save project to "{}".').format(project_path) - ) - - # export the DCIM folder - for attachment_dir in Preferences().value("attachmentDirs"): - copy_attachments( - Path(original_project_path).parent, - project_path.parent, - attachment_dir, - ) - - title = self.project.title() - title_suffix = self.tr("(QFieldCloud)") - if not title.endswith(title_suffix): - self.project.setTitle("{} {}".format(title, title_suffix)) - # Now we have a project state which can be saved as cloud project - self.project.write(str(project_path)) - is_converted = True - finally: - # We need to let the app handle events before loading the next project or QGIS will crash with rasters - QCoreApplication.processEvents() - self.project.clear() - QCoreApplication.processEvents() - - # TODO @suricactus: whatcha gonna do if QgsProject::read()/write() fails - if is_converted: - iface.addProject(str(project_path)) - else: - open_project(original_project_path, backup_project_path) - - self.total_progress_updated.emit(100, 100, self.tr("Finished")) diff --git a/qfieldsync/gui/cloud_create_project_widget.py b/qfieldsync/gui/cloud_create_project_widget.py index 9a9c60a5..7fa5ce1b 100644 --- a/qfieldsync/gui/cloud_create_project_widget.py +++ b/qfieldsync/gui/cloud_create_project_widget.py @@ -22,29 +22,23 @@ import os from pathlib import Path -from typing import Optional from libqfieldsync.layer import LayerSource -from libqfieldsync.utils.file_utils import fileparts, get_unique_empty_dirname -from libqfieldsync.utils.qgis import get_qgis_files_within_dir -from qgis.core import Qgis, QgsApplication, QgsProject +from qgis.core import QgsApplication, QgsProject from qgis.gui import QgisInterface -from qgis.PyQt.QtCore import QDir, QRegularExpression, Qt, QTimer, QUrl, pyqtSignal +from qgis.PyQt.QtCore import QDir, QRegularExpression, QUrl, pyqtSignal from qgis.PyQt.QtGui import QDesktopServices, QIcon, QRegularExpressionValidator from qgis.PyQt.QtWidgets import ( QAction, QApplication, QMenu, - QMessageBox, QToolButton, QWidget, ) from qgis.PyQt.uic import loadUiType from qfieldsync.core.cloud_api import CloudNetworkAccessManager, QfcError -from qfieldsync.core.cloud_converter import CloudConverter from qfieldsync.core.cloud_project import CloudProject, ProjectType -from qfieldsync.core.cloud_transferrer import CloudTransferrer from qfieldsync.core.preferences import Preferences from qfieldsync.gui.cloud_login_dialog import CloudLoginDialog from qfieldsync.gui.storage_widget import StorageWidget @@ -80,10 +74,6 @@ def __init__( self.project = project self.qfield_preferences = Preferences() self.network_manager = network_manager - self.cloud_transferrer: Optional[CloudTransferrer] = None - - # keep a timer reference - self.timer = QTimer(self) if not self.network_manager.is_authenticated(): CloudLoginDialog.show_auth_dialog( @@ -92,8 +82,6 @@ def __init__( else: self.network_manager.projects_cache.refresh() - self.cancelButton.clicked.connect(self.on_cancel_button_clicked) - self.nextButton.clicked.connect(self.on_next_button_clicked) self.backButton.clicked.connect(self.on_back_button_clicked) self.createButton.clicked.connect(self.on_create_button_clicked) self.localDirButton.clicked.connect(self.on_local_dir_button_clicked) @@ -140,73 +128,34 @@ def __init__( self.storage_widget = StorageWidget(self.network_manager, self) self.projectDetailsLayout.addWidget(self.storage_widget, 6, 1) - def restart(self): - self.stackedWidget.setCurrentWidget(self.selectTypePage) + if self.network_manager.is_authenticated(): + self.setup_project_details_page() - if self.network_manager.projects_cache.is_currently_open_project_cloud_local: - self.createCloudRadioButton.setChecked(True) - self.cloudifyRadioButton.setEnabled(False) - self.cloudifyInfoLabel.setEnabled(False) - else: - self.cloudifyRadioButton.setChecked(True) - self.cloudifyRadioButton.setEnabled(True) - self.cloudifyInfoLabel.setEnabled(True) - - def cloudify_project(self): - for cloud_project in self.network_manager.projects_cache.projects: - if cloud_project.name == self.get_cloud_project_name(): - QMessageBox.warning( - None, - self.tr("Warning"), - self.tr( - "The project name is already present in your QFieldCloud repository, please pick a different name." - ), - ) - return - - if get_qgis_files_within_dir(self.localDirLineEdit.text()): - QMessageBox.warning( - None, - self.tr("Warning"), - self.tr( - "The export directory already contains a project file, please pick a different directory." - ), - ) - return + def restart(self) -> None: + self.setup_project_details_page() - QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor) - - self.stackedWidget.setCurrentWidget(self.progressPage) - self.convertProgressBar.setVisible(True) - self.convertLabel.setVisible(True) - self.uploadLabel.setText(self.tr("Uploading project")) - - if not self.project.title(): - self.project.setTitle(self.get_cloud_project_name()) - self.project.setDirty() + def setup_project_details_page(self) -> None: + """Initializes values on projectDetailsPage.""" + if not self.network_manager.is_authenticated(): + return - cloud_convertor = CloudConverter(self.project, self.localDirLineEdit.text()) + project_name = self.get_unique_project_name(self.project) - cloud_convertor.warning.connect(self.on_show_warning) - cloud_convertor.total_progress_updated.connect(self.on_update_total_progressbar) + self.stackedWidget.setCurrentWidget(self.projectDetailsPage) + self.projectNameLineEdit.setText(project_name) + self.projectDescriptionTextEdit.setText(self.project.metadata().abstract()) - def convert(): - try: - cloud_convertor.convert() - except Exception: - QApplication.restoreOverrideCursor() - critical_message = self.tr( - "The project could not be converted into the export directory." - ) - self.iface.messageBar().pushMessage( - critical_message, Qgis.MessageLevel.Critical, 0 - ) - self.close() - return + self.refresh_project_owners() - self.create_cloud_project() + if self.project.fileName(): + default_dir = str(Path(self.project.fileName()).parent) + else: + default_dir = self.qfield_preferences.value("cloudDirectory") or str( + Path.home() + ) - self.timer.singleShot(1, convert) + self.set_dirname(default_dir) + self.update_info_visibility() def get_cloud_project_name(self) -> str: return self.projectNameLineEdit.text() @@ -254,20 +203,8 @@ def on_create_project_finished(self, reply): {**payload, "local_dir": self.localDirLineEdit.text()} ) - if self.createCloudRadioButton.isChecked(): - self.uploadProgressBar.setValue(100) - self.after_project_creation_action(cloud_project.id) - elif self.cloudifyRadioButton.isChecked(): - self.cloud_transferrer = CloudTransferrer( - self.network_manager, cloud_project - ) - self.cloud_transferrer.upload_progress.connect( - self.on_transferrer_update_progress - ) - self.cloud_transferrer.finished.connect( - lambda: self.on_transferrer_finished() - ) - self.cloud_transferrer.sync(list(cloud_project.files_to_sync), [], [], []) + self.uploadProgressBar.setValue(100) + self.after_project_creation_action(cloud_project.id) def after_project_creation_action(self, project_id: str): QApplication.restoreOverrideCursor() @@ -316,21 +253,13 @@ def get_unique_project_name(self, project: QgsProject) -> str: return to_cloud_title(project_name) def set_dirname(self, dirname: str): - if self.cloudifyRadioButton.isChecked(): - feedback, feedback_msg = local_dir_feedback( - dirname, - single_project_status=LocalDirFeedback.Error, - not_existing_status=LocalDirFeedback.Success, - ) - elif self.createCloudRadioButton.isChecked(): - feedback, feedback_msg = local_dir_feedback( - dirname, - no_path_status=LocalDirFeedback.Warning, - ) - else: - raise NotImplementedError("Unknown create new button radio.") + feedback, feedback_msg = local_dir_feedback( + dirname, + no_path_status=LocalDirFeedback.Warning, + ) self.localDirFeedbackLabel.setText(feedback_msg) + self.localDirFeedbackLabel.setVisible(bool(feedback_msg)) if feedback == LocalDirFeedback.Error: self.localDirFeedbackLabel.setStyleSheet("color: red;") @@ -345,15 +274,17 @@ def set_dirname(self, dirname: str): self.localDirLineEdit.setText(QDir.toNativeSeparators(dirname)) def refresh_project_owners(self): + username = self.network_manager.get_username() + if not username or not self.network_manager.is_authenticated(): + return + self.projectOwnerComboBox.setEnabled(False) self.projectOwnerComboBox.clear() - self.projectOwnerComboBox.addItem(self.network_manager.get_username()) + self.projectOwnerComboBox.addItem(username) self.projectOwnerRefreshButton.setEnabled(False) self.projectOwnerFeedbackLabel.setVisible(False) - reply = self.network_manager.get_user_organizations( - self.network_manager.get_username() - ) + reply = self.network_manager.get_user_organizations(username) reply.finished.connect(lambda: self.on_refresh_project_owners_finished(reply)) def on_refresh_project_owners_finished(self, reply): @@ -376,75 +307,7 @@ def on_refresh_project_owners_finished(self, reply): self.projectOwnerComboBox.setEnabled(True) self.projectOwnerRefreshButton.setEnabled(True) - def on_update_total_progressbar(self, current, layer_count, _message): - self.convertProgressBar.setMaximum(layer_count) - self.convertProgressBar.setValue(current) - - def on_transferrer_update_progress(self, fraction): - self.uploadProgressBar.setMaximum(100) - self.uploadProgressBar.setValue(int(fraction * 100)) - - def on_transferrer_finished(self): - result_message = self.tr( - "Finished uploading the project to QFieldCloud, you are now viewing the locally stored copy." - ) - self.iface.messageBar().pushMessage( - result_message, Qgis.MessageLevel.Success, 0 - ) - - self.after_project_creation_action(self.cloud_transferrer.cloud_project.id) - - def on_show_warning(self, _, message): - self.iface.messageBar().pushMessage(message, Qgis.MessageLevel.Warning, 0) - - def on_cancel_button_clicked(self): - self.canceled.emit() - - def on_next_button_clicked(self) -> None: - - project_storage_type = self.project.projectStorage() - - if project_storage_type is not None: - QMessageBox.warning( - None, - self.tr("Warning"), - self.tr( - "The QGIS project file must be stored as a `.qgs`/`.qgz` file! " - f'Storing within a database "{project_storage_type.type()}" is not supported.' - ), - ) - - return - - project_name = self.get_unique_project_name(self.project) - - self.stackedWidget.setCurrentWidget(self.projectDetailsPage) - self.projectNameLineEdit.setText(project_name) - self.projectDescriptionTextEdit.setText(self.project.metadata().abstract()) - - self.refresh_project_owners() - - if self.cloudifyRadioButton.isChecked(): - if project_name: - project_filename = project_name.lower() - else: - project_filename = fileparts(QgsProject.instance().fileName())[1] - - export_dirname = get_unique_empty_dirname( - Path(self.qfield_preferences.value("cloudDirectory")).joinpath( - project_filename - ) - ) - - self.createButton.setEnabled(True) - self.set_dirname(str(export_dirname)) - elif self.createCloudRadioButton.isChecked(): - if self.project.fileName(): - self.set_dirname(str(Path(self.project.fileName()).parent)) - - self.update_info_visibility() - - def on_project_owner_changed(self): + def on_project_owner_changed(self) -> None: if not self.projectOwnerComboBox.currentText(): return @@ -455,15 +318,15 @@ def on_project_owner_refresh_button_click(self): self.refresh_project_owners() def on_back_button_clicked(self): - self.stackedWidget.setCurrentWidget(self.selectTypePage) + self.canceled.emit() def on_create_button_clicked(self): - if self.cloudifyRadioButton.isChecked(): - self.infoLabel.setText(self.cloudifyInfoLabel.text()) - self.cloudify_project() - elif self.createCloudRadioButton.isChecked(): - self.infoLabel.setText(self.createCloudInfoLabel.text()) - self.create_empty_cloud_project() + self.infoLabel.setText( + self.tr( + "A new blank QFieldCloud project will be created. Project files will only be uploaded when you click the synchronize button." + ) + ) + self.create_empty_cloud_project() def on_local_dir_button_clicked(self): dirname = self.cloud_projects_dialog.select_local_dir() diff --git a/qfieldsync/gui/cloud_projects_dialog.py b/qfieldsync/gui/cloud_projects_dialog.py index b543cd78..a212b2ce 100644 --- a/qfieldsync/gui/cloud_projects_dialog.py +++ b/qfieldsync/gui/cloud_projects_dialog.py @@ -831,6 +831,19 @@ def on_projects_table_cell_double_clicked(self) -> None: self.show_project_form() def on_create_button_clicked(self) -> None: + project_storage_type = QgsProject.instance().projectStorage() + + if project_storage_type is not None: + QMessageBox.warning( + self, + self.tr("Warning"), + self.tr( + "The QGIS project file must be stored as a `.qgs`/`.qgz` file! " + f'Storing within a database "{project_storage_type.type()}" is not supported.' + ), + ) + return + self.show_create_project() def show_project_form(self) -> None: diff --git a/qfieldsync/ui/cloud_create_project_widget.ui b/qfieldsync/ui/cloud_create_project_widget.ui index 17d2dc8f..025ca0d4 100644 --- a/qfieldsync/ui/cloud_create_project_widget.ui +++ b/qfieldsync/ui/cloud_create_project_widget.ui @@ -11,7 +11,7 @@ - Convert to QFieldCloud Project + Create QFieldCloud Project @@ -31,123 +31,6 @@ 0 - - - - - - Choose how to create a new project - - - - - - - true - - - - Convert currently open project to cloud project (recommended) - - - true - - - - - - - - true - - - - A new QFieldCloud-compatible project is created from the currently opened QGIS project. In order to do so, datasets will be copied into an export directory that will act as your local mirror. Vector datasets will be converted to geopackage format to facilitate data synchronization from multiple devices while other dataset types will be copied to the new project location. - - - true - - - - - - - - true - - - - Create a new empty QFieldCloud project - - - - - - - - true - - - - A new blank QFieldCloud project will be created. You will be responsible to move all the project-related files within the selected local directory, with the project file at its root. Project files will only be uploaded when you click the synchronize button. Make sure the selected directory contains no more than one QGIS project file. Use when you want to keep a direct connection between <a href="https://docs.qfield.org/get-started/tutorials/advanced-setup-qfc/#postgis">QFieldCloud and PostGIS.</a> - - - true - - - true - - - - - - - - - - - - Cancel - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Next - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - -