diff --git a/qfieldsync/core/cloud_api.py b/qfieldsync/core/cloud_api.py index f0b3feaf..595e465c 100644 --- a/qfieldsync/core/cloud_api.py +++ b/qfieldsync/core/cloud_api.py @@ -154,7 +154,6 @@ def server_urls() -> List[str]: ] def auth(self) -> QgsAuthMethodConfig: - self.url auth_manager = QgsApplication.authManager() if not auth_manager.masterPasswordHashInDatabase(): @@ -273,7 +272,7 @@ def create_project( ) def update_project( - self, project_id: str, name: str, owner: str, description: str, private: bool + self, project_id: str, name: str, description: str ) -> QNetworkReply: """Update an existing QFieldCloud project""" @@ -281,9 +280,7 @@ def update_project( ["projects", project_id], { "name": name, - "owner": owner, "description": description, - "private": private, }, ) diff --git a/qfieldsync/core/cloud_converter.py b/qfieldsync/core/cloud_converter.py index 350bbefe..ad42bf40 100644 --- a/qfieldsync/core/cloud_converter.py +++ b/qfieldsync/core/cloud_converter.py @@ -121,9 +121,10 @@ def convert(self) -> None: # noqa: C901 str(project_path.parent.joinpath("DCIM")), ) - self.project.setTitle( - self.tr("{} (QFieldCloud)").format(self.project.title()) - ) + 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 diff --git a/qfieldsync/core/cloud_transferrer.py b/qfieldsync/core/cloud_transferrer.py index 1ac931a7..c070b661 100644 --- a/qfieldsync/core/cloud_transferrer.py +++ b/qfieldsync/core/cloud_transferrer.py @@ -71,6 +71,9 @@ def __init__( self.replies = [] self.temp_dir = Path(cloud_project.local_dir).joinpath(".qfieldsync") self.error_message = None + self.throttled_uploader = None + self.throttled_downloader = None + self.throttled_deleter = None if self.temp_dir.exists(): shutil.rmtree(self.temp_dir) diff --git a/qfieldsync/gui/cloud_converter_dialog.py b/qfieldsync/gui/cloud_converter_dialog.py deleted file mode 100644 index 87f7310c..00000000 --- a/qfieldsync/gui/cloud_converter_dialog.py +++ /dev/null @@ -1,275 +0,0 @@ -# -*- coding: utf-8 -*- -""" -/*************************************************************************** - QFieldCloudConverterDialog - A QGIS plugin - Sync your projects to QField on android - ------------------- - begin : 2021-057-22 - git sha : $Format:%H$ - copyright : (C) 2015 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. * - * * - ***************************************************************************/ -""" - -import os -import re -from pathlib import Path -from typing import Optional - -from qgis.core import Qgis, QgsProject -from qgis.gui import QgisInterface -from qgis.PyQt.QtCore import QDir, Qt -from qgis.PyQt.QtWidgets import ( - QApplication, - QDialog, - QDialogButtonBox, - QMessageBox, - QWidget, -) -from qgis.PyQt.uic import loadUiType - -from qfieldsync.core.cloud_api import ( - CloudException, - CloudNetworkAccessManager, - from_reply, -) -from qfieldsync.core.cloud_converter import CloudConverter -from qfieldsync.core.cloud_project import CloudProject -from qfieldsync.core.cloud_transferrer import CloudTransferrer -from qfieldsync.core.preferences import Preferences -from qfieldsync.gui.cloud_login_dialog import CloudLoginDialog -from qfieldsync.libqfieldsync.layer import LayerSource -from qfieldsync.libqfieldsync.utils.file_utils import ( - fileparts, - get_unique_empty_dirname, -) -from qfieldsync.utils.qgis_utils import get_qgis_files_within_dir - -from ..utils.qt_utils import make_folder_selector - -DialogUi, _ = loadUiType( - os.path.join(os.path.dirname(__file__), "../ui/cloud_converter_dialog.ui") -) - - -class CloudConverterDialog(QDialog, DialogUi): - def __init__( - self, - iface: QgisInterface, - network_manager: CloudNetworkAccessManager, - project: QgsProject, - parent: QWidget = None, - ) -> None: - """Constructor.""" - super(CloudConverterDialog, self).__init__(parent=parent) - self.setupUi(self) - - self.iface = iface - self.project = project - self.qfield_preferences = Preferences() - self.network_manager = network_manager - self.cloud_transferrer: Optional[CloudTransferrer] = None - - if not self.network_manager.has_token(): - CloudLoginDialog.show_auth_dialog( - self.network_manager, lambda: self.close(), None, parent=self - ) - else: - self.network_manager.projects_cache.refresh() - - project_name = self.project.baseName() - if project_name: - pattern = re.compile(r"[\W_]+") - project_name = pattern.sub("", project_name) - else: - project_name = "CloudProject" - - project_name = self.network_manager.projects_cache.get_unique_name(project_name) - - self.mProjectName.setText(project_name) - self.button_box.button(QDialogButtonBox.Save).setText(self.tr("Create")) - self.button_box.button(QDialogButtonBox.Save).clicked.connect( - self.convert_project - ) - - self.projectGroupBox.setVisible(True) - self.progressGroupBox.setVisible(False) - - self.setup_gui() - - def setup_gui(self): - """Populate gui and connect signals of the push dialog""" - project_filename = QgsProject.instance().fileName() - export_dirname = Path(self.qfield_preferences.value("cloudDirectory")).joinpath( - fileparts(project_filename)[1] if project_filename else "new_cloud_project" - ) - export_dirname = get_unique_empty_dirname(export_dirname) - - self.exportDirLineEdit.setText(QDir.toNativeSeparators(str(export_dirname))) - self.exportDirButton.clicked.connect( - make_folder_selector(self.exportDirLineEdit) - ) - self.update_info_visibility() - - def get_export_folder_from_dialog(self): - """Get the export folder according to the inputs in the selected""" - return self.exportDirLineEdit.text() - - def convert_project(self): - assert self.network_manager.projects_cache.projects - - for cloud_project in self.network_manager.projects_cache.projects: - if cloud_project.name == self.mProjectName.text(): - 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.exportDirLineEdit.text()): - QMessageBox.warning( - None, - self.tr("Warning"), - self.tr( - "The export directory already contains a project file, please pick a different directory." - ), - ) - return - - QApplication.setOverrideCursor(Qt.WaitCursor) - - self.button_box.setEnabled(False) - self.projectGroupBox.setVisible(False) - self.progressGroupBox.setVisible(True) - - if not self.project.title(): - self.project.setTitle(self.get_cloud_project_name()) - self.project.setDirty() - - cloud_convertor = CloudConverter( - self.project, self.get_export_folder_from_dialog() - ) - - cloud_convertor.warning.connect(self.on_show_warning) - cloud_convertor.total_progress_updated.connect(self.on_update_total_progressbar) - - 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.Critical, 0) - self.close() - return - - self.create_cloud_project() - - def get_cloud_project_name(self) -> str: - pattern = re.compile(r"[\W_]+") - return pattern.sub("", self.mProjectName.text()) - - def create_cloud_project(self): - reply = self.network_manager.create_project( - self.get_cloud_project_name(), - self.network_manager.auth().config("username"), - self.project.metadata().abstract(), - True, - ) - reply.finished.connect(lambda: self.on_create_project_finished(reply)) - - def on_create_project_finished(self, reply): - try: - payload = self.network_manager.json_object(reply) - except CloudException as err: - QApplication.restoreOverrideCursor() - critical_message = self.tr( - "QFieldCloud rejected projection creation: {}" - ).format(from_reply(err.reply)) - self.iface.messageBar().pushMessage(critical_message, Qgis.Critical, 0) - self.close() - return - - # save `local_dir` configuration permanently, `CloudProject` constructor does this for free - cloud_project = CloudProject( - {**payload, "local_dir": self.get_export_folder_from_dialog()} - ) - - 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(self.on_transferrer_finished) - self.cloud_transferrer.sync(list(cloud_project.files_to_sync), [], []) - - def do_post_cloud_convert_action(self): - QApplication.restoreOverrideCursor() - - self.network_manager.projects_cache.refresh() - - result_message = self.tr( - "Finished converting the project to QFieldCloud, you are now view its locally stored copy." - ) - self.iface.messageBar().pushMessage(result_message, Qgis.Success, 0) - self.close() - - def update_info_visibility(self): - """ - Show the info label if there are unconfigured layers - """ - localizedDataPathLayers = [] - for layer in list(self.project.mapLayers().values()): - layer_source = LayerSource(layer) - if layer.dataProvider() is not None: - if layer_source.is_localized_path: - localizedDataPathLayers.append( - "- {} ({})".format(layer.name(), layer_source.filename) - ) - - if localizedDataPathLayers: - if len(localizedDataPathLayers) == 1: - self.infoLocalizedLayersLabel.setText( - self.tr("The layer stored in a localized data path is:\n{}").format( - "\n".join(localizedDataPathLayers) - ) - ) - else: - self.infoLocalizedLayersLabel.setText( - self.tr( - "The layers stored in a localized data path are:\n{}" - ).format("\n".join(localizedDataPathLayers)) - ) - self.infoLocalizedLayersLabel.setVisible(True) - self.infoLocalizedPresentLabel.setVisible(True) - else: - self.infoLocalizedLayersLabel.setVisible(False) - self.infoLocalizedPresentLabel.setVisible(False) - self.infoGroupBox.setVisible(len(localizedDataPathLayers) > 0) - - def on_update_total_progressbar(self, current, layer_count, message): - self.totalProgressBar.setMaximum(layer_count) - self.totalProgressBar.setValue(current) - - def on_transferrer_update_progress(self, fraction): - self.uploadProgressBar.setMaximum(100) - self.uploadProgressBar.setValue(int(fraction * 100)) - - def on_transferrer_finished(self): - self.do_post_cloud_convert_action() - - def on_show_warning(self, _, message): - self.iface.messageBar().pushMessage(message, Qgis.Warning, 0) diff --git a/qfieldsync/gui/cloud_create_project_widget.py b/qfieldsync/gui/cloud_create_project_widget.py new file mode 100644 index 00000000..da468e32 --- /dev/null +++ b/qfieldsync/gui/cloud_create_project_widget.py @@ -0,0 +1,391 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + QFieldCloudCreateProjectWidget + A QGIS plugin + Sync your projects to QField on android + ------------------- + begin : 2021-057-22 + git sha : $Format:%H$ + copyright : (C) 2015 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. * + * * + ***************************************************************************/ +""" + +import os +import re +from pathlib import Path +from typing import Optional + +from qgis.core import Qgis, QgsProject +from qgis.gui import QgisInterface +from qgis.PyQt.QtCore import QRegularExpression, Qt, pyqtSignal +from qgis.PyQt.QtGui import 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 ( + CloudException, + CloudNetworkAccessManager, + from_reply, +) +from qfieldsync.core.cloud_converter import CloudConverter +from qfieldsync.core.cloud_project import CloudProject +from qfieldsync.core.cloud_transferrer import CloudTransferrer +from qfieldsync.core.preferences import Preferences +from qfieldsync.gui.cloud_login_dialog import CloudLoginDialog +from qfieldsync.libqfieldsync.layer import LayerSource +from qfieldsync.libqfieldsync.utils.file_utils import ( + fileparts, + get_unique_empty_dirname, +) +from qfieldsync.libqfieldsync.utils.qgis import get_qgis_files_within_dir +from qfieldsync.utils.cloud_utils import ( + LocalDirFeedback, + local_dir_feedback, + to_cloud_title, +) + +WidgetUi, _ = loadUiType( + os.path.join(os.path.dirname(__file__), "../ui/cloud_create_project_widget.ui") +) + + +class CloudCreateProjectWidget(QWidget, WidgetUi): + finished = pyqtSignal() + canceled = pyqtSignal() + + def __init__( + self, + iface: QgisInterface, + network_manager: CloudNetworkAccessManager, + project: QgsProject, + parent: QWidget, + ) -> None: + """Constructor.""" + super(CloudCreateProjectWidget, self).__init__(parent=parent) + self.setupUi(self) + + self.cloud_projects_dialog = parent + self.iface = iface + self.project = project + self.qfield_preferences = Preferences() + self.network_manager = network_manager + self.cloud_transferrer: Optional[CloudTransferrer] = None + + if not self.network_manager.has_token(): + CloudLoginDialog.show_auth_dialog( + self.network_manager, lambda: self.close(), None, parent=self + ) + 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.dirnameButton.clicked.connect(self.on_dirname_button_clicked) + self.dirnameLineEdit.textChanged.connect(self.on_dirname_line_edit_text_changed) + + self.use_current_project_directory_action = QAction( + QIcon(), self.tr("Use Current Project Directory") + ) + self.use_current_project_directory_action.triggered.connect( + self.on_use_current_project_directory_action_triggered + ) + self.dirnameButton.setMenu(QMenu()) + self.dirnameButton.setPopupMode(QToolButton.MenuButtonPopup) + self.dirnameButton.menu().addAction(self.use_current_project_directory_action) + + self.projectNameLineEdit.setValidator( + QRegularExpressionValidator( + QRegularExpression("^[a-zA-Z][-a-zA-Z0-9_]{2,}$") + ) + ) + + def restart(self): + self.stackedWidget.setCurrentWidget(self.selectTypePage) + + 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): + assert self.network_manager.projects_cache.projects + + 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.dirnameLineEdit.text()): + QMessageBox.warning( + None, + self.tr("Warning"), + self.tr( + "The export directory already contains a project file, please pick a different directory." + ), + ) + return + + QApplication.setOverrideCursor(Qt.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() + + cloud_convertor = CloudConverter(self.project, self.dirnameLineEdit.text()) + + cloud_convertor.warning.connect(self.on_show_warning) + cloud_convertor.total_progress_updated.connect(self.on_update_total_progressbar) + + 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.Critical, 0) + self.close() + return + + self.create_cloud_project() + + def get_cloud_project_name(self) -> str: + pattern = re.compile(r"[\W_]+") + return pattern.sub("", self.projectNameLineEdit.text()) + + def create_empty_cloud_project(self): + self.convertProgressBar.setVisible(False) + self.convertLabel.setVisible(False) + self.uploadLabel.setText(self.tr("Creating project")) + + self.create_cloud_project() + + def create_cloud_project(self): + self.stackedWidget.setCurrentWidget(self.progressPage) + + if not self.project.title(): + self.project.setTitle(self.get_cloud_project_name()) + self.project.setDirty() + + reply = self.network_manager.create_project( + self.get_cloud_project_name(), + self.network_manager.auth().config("username"), + self.project.metadata().abstract(), + True, + ) + reply.finished.connect(lambda: self.on_create_project_finished(reply)) + + def on_create_project_finished(self, reply): + try: + payload = self.network_manager.json_object(reply) + except CloudException as err: + QApplication.restoreOverrideCursor() + critical_message = self.tr( + "QFieldCloud rejected project creation: {}" + ).format(from_reply(err.reply)) + self.iface.messageBar().pushMessage(critical_message, Qgis.Critical, 0) + self.close() + return + # save `local_dir` configuration permanently, `CloudProject` constructor does this for free + cloud_project = CloudProject( + {**payload, "local_dir": self.dirnameLineEdit.text()} + ) + + if self.createCloudRadioButton.isChecked(): + self.uploadProgressBar.setValue(100) + self.after_project_creation_action() + 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), [], []) + + def after_project_creation_action(self): + QApplication.restoreOverrideCursor() + + self.network_manager.projects_cache.refresh() + + 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.Success, 0) + self.finished.emit() + + def update_info_visibility(self): + """ + Show the info label if there are unconfigured layers + """ + localizedDataPathLayers = [] + for layer in list(self.project.mapLayers().values()): + layer_source = LayerSource(layer) + if layer.dataProvider() is not None: + if layer_source.is_localized_path: + localizedDataPathLayers.append( + "- {} ({})".format(layer.name(), layer_source.filename) + ) + + if localizedDataPathLayers: + if len(localizedDataPathLayers) == 1: + self.infoLocalizedLayersLabel.setText( + self.tr("The layer stored in a localized data path is:\n{}").format( + "\n".join(localizedDataPathLayers) + ) + ) + else: + self.infoLocalizedLayersLabel.setText( + self.tr( + "The layers stored in a localized data path are:\n{}" + ).format("\n".join(localizedDataPathLayers)) + ) + self.infoLocalizedLayersLabel.setVisible(True) + self.infoLocalizedPresentLabel.setVisible(True) + else: + self.infoLocalizedLayersLabel.setVisible(False) + self.infoLocalizedPresentLabel.setVisible(False) + self.infoGroupBox.setVisible(len(localizedDataPathLayers) > 0) + + def get_unique_project_name(self, project: QgsProject) -> str: + project_name = to_cloud_title(QgsProject.instance().title()) + + if not project_name: + project_name = project.baseName() + + if not project_name: + project_name = "UntitledCloudProject" + + project_name = ( + self.network_manager.projects_cache.get_unique_name(project_name) or "" + ) + + return 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) + else: + raise NotImplementedError("Unknown create new button radio.") + + self.dirnameFeedbackLabel.setText(feedback_msg) + + if feedback == LocalDirFeedback.Error: + self.dirnameFeedbackLabel.setStyleSheet("color: red;") + self.createButton.setEnabled(False) + elif feedback == LocalDirFeedback.Warning: + self.dirnameFeedbackLabel.setStyleSheet("color: orange;") + self.createButton.setEnabled(True) + else: + self.dirnameFeedbackLabel.setStyleSheet("color: green;") + self.createButton.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): + self.after_project_creation_action() + + def on_show_warning(self, _, message): + self.iface.messageBar().pushMessage(message, Qgis.Warning, 0) + + def on_cancel_button_clicked(self): + self.canceled.emit() + + def on_next_button_clicked(self) -> None: + 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()) + + project_filename = ( + project_name.lower() + if project_name + else fileparts(QgsProject.instance().fileName())[1] + ) + export_dirname = get_unique_empty_dirname( + Path(self.qfield_preferences.value("cloudDirectory")).joinpath( + project_filename + ) + ) + if self.cloudifyRadioButton.isChecked(): + self.createButton.setEnabled(True) + self.dirnameLineEdit.setText(str(export_dirname)) + elif self.createCloudRadioButton.isChecked(): + self.dirnameLineEdit.setText(str(Path(self.project.fileName()).parent)) + + self.update_info_visibility() + + def on_back_button_clicked(self): + self.stackedWidget.setCurrentWidget(self.selectTypePage) + + 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() + + def on_dirname_button_clicked(self): + dirname = self.cloud_projects_dialog.select_local_dir() + + if dirname: + self.set_dirname(dirname) + self.dirnameLineEdit.setText(str(Path(dirname))) + + def on_dirname_line_edit_text_changed(self, text: str): + self.set_dirname(self.dirnameLineEdit.text()) + + def on_use_current_project_directory_action_triggered(self): + self.dirnameLineEdit.setText(str(Path(self.project.fileName()).parent)) diff --git a/qfieldsync/gui/cloud_login_dialog.py b/qfieldsync/gui/cloud_login_dialog.py index 0f69578a..8e7487dd 100644 --- a/qfieldsync/gui/cloud_login_dialog.py +++ b/qfieldsync/gui/cloud_login_dialog.py @@ -47,8 +47,6 @@ def show_auth_dialog( parent: QWidget = None, ): if CloudLoginDialog.instance: - if parent: - CloudLoginDialog.instance.setParent(parent) CloudLoginDialog.instance.show() return @@ -111,6 +109,7 @@ def __init__( self.qfieldCloudIcon.mouseDoubleClickEvent = ( lambda event: self.toggleServerUrlVisibility() ) + self.hide() def toggleServerUrlVisibility(self): self.serverUrlLabel.setVisible(not self.serverUrlLabel.isVisible()) diff --git a/qfieldsync/gui/cloud_projects_dialog.py b/qfieldsync/gui/cloud_projects_dialog.py index 85d1cae2..625ab584 100644 --- a/qfieldsync/gui/cloud_projects_dialog.py +++ b/qfieldsync/gui/cloud_projects_dialog.py @@ -20,11 +20,10 @@ * * ***************************************************************************/ """ -from enum import Enum from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Callable, Optional -from qgis.core import Qgis, QgsApplication, QgsProject +from qgis.core import QgsApplication, QgsProject from qgis.PyQt.QtCore import ( QDateTime, QItemSelectionModel, @@ -34,6 +33,7 @@ pyqtSignal, ) from qgis.PyQt.QtGui import ( + QDesktopServices, QFont, QIcon, QPixmap, @@ -42,6 +42,7 @@ ) from qgis.PyQt.QtNetwork import QNetworkReply from qgis.PyQt.QtWidgets import ( + QAbstractItemView, QAction, QCheckBox, QDialog, @@ -61,18 +62,13 @@ from qgis.utils import iface from qfieldsync.core import Preferences -from qfieldsync.core.cloud_api import ( - CloudException, - CloudNetworkAccessManager, - from_reply, -) +from qfieldsync.core.cloud_api import CloudException, CloudNetworkAccessManager from qfieldsync.core.cloud_project import CloudProject, ProjectFile, ProjectFileCheckout -from qfieldsync.gui.cloud_converter_dialog import CloudConverterDialog +from qfieldsync.gui.cloud_create_project_widget import CloudCreateProjectWidget from qfieldsync.gui.cloud_login_dialog import CloudLoginDialog from qfieldsync.gui.cloud_transfer_dialog import CloudTransferDialog -from qfieldsync.libqfieldsync.utils.qgis import get_qgis_files_within_dir -from qfieldsync.utils.cloud_utils import closure, to_cloud_title -from qfieldsync.utils.permissions import can_change_project_owner, can_delete_project +from qfieldsync.utils.cloud_utils import LocalDirFeedback, closure, local_dir_feedback +from qfieldsync.utils.permissions import can_delete_project from qfieldsync.utils.qt_utils import rounded_pixmap CloudProjectsDialogUi, _ = loadUiType( @@ -80,12 +76,6 @@ ) -class LocalDirFeedback(Enum): - Error = "error" - Warning = "warning" - Success = "success" - - class CloudProjectsDialog(QDialog, CloudProjectsDialogUi): projects_refreshed = pyqtSignal() _current_cloud_project = None @@ -138,9 +128,9 @@ def __init__( self.projectsType.setCurrentIndex(0) self.projectsType.currentIndexChanged.connect(lambda: self.show_projects()) - self.projectsTable.setColumnWidth(0, self.projectsTable.width() / 2) - self.projectsTable.setColumnWidth(1, self.projectsTable.width() / 3.25) - self.projectsTable.setColumnWidth(2, self.projectsTable.width() / 10) + self.projectsTable.setColumnWidth(0, int(self.projectsTable.width() / 2)) + self.projectsTable.setColumnWidth(1, int(self.projectsTable.width() / 3.25)) + self.projectsTable.setColumnWidth(2, int(self.projectsTable.width() / 10)) self.synchronizeButton.clicked.connect( lambda: self.on_project_sync_button_clicked() @@ -159,13 +149,28 @@ def __init__( ) self.deleteButton.setEnabled(False) + self.projectsStack.setCurrentWidget(self.projectsListPage) + self.createProjectWidget = CloudCreateProjectWidget( + iface, + self.network_manager, + QgsProject.instance(), + self, + ) + self.projectCreatePage.layout().addWidget(self.createProjectWidget) + self.createProjectWidget.finished.connect( + lambda: self.on_create_project_finished() + ) + self.createProjectWidget.canceled.connect( + lambda: self.on_create_project_canceled() + ) + self.refreshButton.setIcon(QgsApplication.getThemeIcon("/mActionRefresh.svg")) self.refreshButton.clicked.connect(lambda: self.on_refresh_button_clicked()) self.createButton.clicked.connect(lambda: self.on_create_button_clicked()) - self.convertButton.clicked.connect(lambda: self.on_convert_button_clicked()) self.backButton.clicked.connect(lambda: self.on_back_button_clicked()) self.submitButton.clicked.connect(lambda: self.on_submit_button_clicked()) + self.editOnlineButton.clicked.connect(self.on_edit_online_button_clicked) self.projectsTable.cellDoubleClicked.connect( lambda: self.on_projects_table_cell_double_clicked() ) @@ -188,9 +193,6 @@ def __init__( self.localDirLineEdit.editingFinished.connect( lambda: self.on_local_dir_line_edit_editing_finished() ) - self.projectOwnerRefreshButton.clicked.connect( - lambda: self.on_project_owner_refresh_button_clicked() - ) self.localDirButton.clicked.connect(lambda: self.on_local_dir_button_clicked()) self.localDirButton.setMenu(QMenu()) self.localDirButton.setPopupMode(QToolButton.MenuButtonPopup) @@ -507,19 +509,18 @@ def on_button_box_clicked(self) -> None: def on_local_dir_line_edit_text_changed(self) -> None: local_dir = self.localDirLineEdit.text() - self.submitButton.setEnabled(False) - - feedback, feedback_msg = self.local_dir_feedback(local_dir) + feedback, feedback_msg = local_dir_feedback(local_dir) self.localDirFeedbackLabel.setText(feedback_msg) if feedback == LocalDirFeedback.Error: self.localDirFeedbackLabel.setStyleSheet("color: red;") + self.submitButton.setEnabled(False) elif feedback == LocalDirFeedback.Warning: self.localDirFeedbackLabel.setStyleSheet("color: orange;") + self.submitButton.setEnabled(True) else: self.localDirFeedbackLabel.setStyleSheet("color: green;") - - self.submitButton.setEnabled(True) + self.submitButton.setEnabled(True) def on_local_dir_line_edit_editing_finished(self) -> None: local_dir = self.localDirLineEdit.text() @@ -532,16 +533,6 @@ def on_local_dir_button_clicked(self) -> None: if dirname: self.localDirLineEdit.setText(str(Path(dirname))) - def on_project_owner_refresh_button_clicked(self) -> None: - self.request_refresh_project_owners_combobox() - - def on_get_user_organizations_finished(self, reply: QNetworkReply) -> None: - try: - payload = self.network_manager.json_array(reply) - self.refresh_project_owners_combobox(payload) - except Exception: - self.feedbackLabel.setText(self.tr("Failed to refresh project owners.")) - def on_logout_button_clicked(self) -> None: self.buttonBox.button(QDialogButtonBox.Reset).setEnabled(False) self.feedbackLabel.setVisible(False) @@ -550,85 +541,6 @@ def on_logout_button_clicked(self) -> None: def on_refresh_button_clicked(self) -> None: self.network_manager.projects_cache.refresh() - def refresh_project_owners_combobox( - self, organizations: List[Dict[str, Any]] = [] - ) -> None: - username = self.network_manager.auth().config("username") - selected_value = username - is_project_owner_select_enabled = True - - if self.current_cloud_project: - selected_value = self.current_cloud_project.owner - is_project_owner_select_enabled = can_change_project_owner( - self.current_cloud_project - ) - - self.projectOwnerComboBox.clear() - self.projectOwnerComboBox.setEnabled(is_project_owner_select_enabled) - self.projectOwnerRefreshButton.setEnabled(is_project_owner_select_enabled) - self.projectOwnerComboBox.addItem(username, username) - - if organizations: - for org in organizations: - self.projectOwnerComboBox.addItem(org["username"], org["username"]) - - selected_value_idx = self.projectOwnerComboBox.findData(selected_value) - if selected_value_idx == -1: - selected_value_idx = 0 - self.projectOwnerComboBox.insertItem( - selected_value_idx, - selected_value, - selected_value, - ) - - self.projectOwnerComboBox.setCurrentIndex(selected_value_idx) - - def request_refresh_project_owners_combobox(self) -> None: - self.projectOwnerRefreshButton.setEnabled(False) - - reply = self.network_manager.get_user_organizations( - self.network_manager.auth().config("username") - ) - reply.finished.connect(lambda: self.on_get_user_organizations_finished(reply)) - - def local_dir_feedback( - self, local_dir, empty_ok=True, exiting_ok=True - ) -> Tuple[LocalDirFeedback, str]: - if not local_dir: - return LocalDirFeedback.Error, self.tr( - "Please select local directory where the project to be stored." - ) - elif not Path(local_dir).is_dir(): - return LocalDirFeedback.Warning, self.tr( - "The entered path is not an existing directory. It will be created after you submit this form." - ) - elif len(get_qgis_files_within_dir(Path(local_dir))) == 0: - message = self.tr( - "The entered path does not contain a QGIS project file yet." - ) - status = LocalDirFeedback.Warning - - if empty_ok: - status = LocalDirFeedback.Success - message += " " - message += self.tr("You can always add one later.") - - return status, message - elif len(get_qgis_files_within_dir(Path(local_dir))) == 1: - message = self.tr("The entered path contains one QGIS project file.") - status = LocalDirFeedback.Warning - - if exiting_ok: - status = LocalDirFeedback.Success - message += " " - message += self.tr("Exactly as it should be.") - - return status, message - else: - return LocalDirFeedback.Error, self.tr( - "Multiple project files have been found in the directory. Please leave exactly one QGIS project in the root directory." - ) - def show_projects(self) -> None: self.feedbackLabel.setText("") self.feedbackLabel.setVisible(False) @@ -770,8 +682,8 @@ def select_local_dir(self) -> Optional[str]: if local_dir == "": return - feedback, feedback_msg = self.local_dir_feedback( - local_dir, empty_ok=False + feedback, feedback_msg = local_dir_feedback( + local_dir, no_project_status=LocalDirFeedback.Warning ) title = self.tr("Cannot upload local QFieldSync directory") @@ -804,8 +716,8 @@ def select_local_dir(self) -> Optional[str]: # when the dir is empty, all is good. But if not there are some file, we need to ask the user to confirm what to do if list(Path(local_dir).iterdir()): buttons = QMessageBox.Ok | QMessageBox.Abort - feedback, feedback_msg = self.local_dir_feedback( - local_dir, exiting_ok=False + feedback, feedback_msg = local_dir_feedback( + local_dir, single_project_status=LocalDirFeedback.Warning ) title = self.tr("QFieldSync checkout prefers an empty directory") answer = None @@ -869,32 +781,11 @@ def on_projects_table_cell_double_clicked(self) -> None: self.show_project_form() def on_create_button_clicked(self) -> None: - self.projectsTable.clearSelection() - self.current_cloud_project = None - self.show_project_form() - - def on_convert_button_clicked(self) -> None: - if QgsProject.instance().mapLayers(): - self.cloud_convert_dlg = CloudConverterDialog( - iface, - self.network_manager, - QgsProject.instance(), - self, - ) - self.cloud_convert_dlg.setAttribute(Qt.WA_DeleteOnClose) - self.cloud_convert_dlg.setWindowFlags( - self.cloud_convert_dlg.windowFlags() | Qt.Tool - ) - self.cloud_convert_dlg.open() - self.update_ui_state() - else: - iface.messageBar().pushMessage( - self.tr("At least one layer is required to convert a project."), - Qgis.Warning, - 5, - ) + self.show_create_project() def show_project_form(self) -> None: + assert self.current_cloud_project + self.show() self.projectsStack.setCurrentWidget(self.projectsFormPage) @@ -902,83 +793,59 @@ def show_project_form(self) -> None: self.projectFilesTree.clear() self.projectNameLineEdit.setEnabled(True) self.projectDescriptionTextEdit.setEnabled(True) - self.projectIsPrivateCheckBox.setEnabled(True) - self.projectOwnerComboBox.setEnabled(True) - - self.refresh_project_owners_combobox() - self.request_refresh_project_owners_combobox() - - if self.current_cloud_project is None: - self.submitButton.setText(self.tr("Create new project")) - self.projectTabs.setTabEnabled(1, False) - self.projectTabs.setTabEnabled(2, False) - self.projectNameLineEdit.setText( - to_cloud_title(QgsProject.instance().title()) - ) - self.projectDescriptionTextEdit.setPlainText("") - self.projectIsPrivateCheckBox.setChecked(True) - # check if there is already another cloud project using the currently open filename - if CloudProject.get_cloud_project_id(QgsProject.instance().homePath()): - self.localDirLineEdit.setText("") - else: - self.localDirLineEdit.setText( - str(Path(QgsProject().instance().homePath())) - ) - - else: - self.submitButton.setText(self.tr("Update project details")) - self.projectTabs.setTabEnabled(1, True) - self.projectTabs.setTabEnabled(2, True) - # TODO validate project name to match QFieldCloudRequirements - self.projectNameLineEdit.setText(self.current_cloud_project.name) - self.projectDescriptionTextEdit.setPlainText( - self.current_cloud_project.description - ) - self.projectIsPrivateCheckBox.setChecked( - self.current_cloud_project.is_private - ) - self.localDirLineEdit.setText(self.current_cloud_project.local_dir) - self.projectUrlLabelValue.setText( - '{url}'.format( - url=(self.network_manager.url + self.current_cloud_project.url) - ) - ) - self.createdAtLabelValue.setText( - QDateTime.fromString( - self.current_cloud_project.created_at, Qt.ISODateWithMs - ).toString() - ) - self.updatedAtLabelValue.setText( - QDateTime.fromString( - self.current_cloud_project.updated_at, Qt.ISODateWithMs - ).toString() - ) - self.lastSyncedAtLabelValue.setText( - QDateTime.fromString( - self.current_cloud_project.updated_at, Qt.ISODateWithMs - ).toString() + self.projectTabs.setTabEnabled(1, True) + self.projectTabs.setTabEnabled(2, True) + self.projectNameLineEdit.setText(self.current_cloud_project.name) + self.projectDescriptionTextEdit.setPlainText( + self.current_cloud_project.description + ) + self.projectIsPrivateCheckBox.setChecked(self.current_cloud_project.is_private) + self.projectOwnerLineEdit.setText(self.current_cloud_project.owner) + self.localDirLineEdit.setText(self.current_cloud_project.local_dir) + self.projectUrlLabelValue.setText( + '{url}'.format( + url=(self.network_manager.url + self.current_cloud_project.url) ) + ) + self.createdAtLabelValue.setText( + QDateTime.fromString( + self.current_cloud_project.created_at, Qt.ISODateWithMs + ).toString() + ) + self.updatedAtLabelValue.setText( + QDateTime.fromString( + self.current_cloud_project.updated_at, Qt.ISODateWithMs + ).toString() + ) + self.lastSyncedAtLabelValue.setText( + QDateTime.fromString( + self.current_cloud_project.updated_at, Qt.ISODateWithMs + ).toString() + ) - if self.current_cloud_project.user_role not in ("admin", "manager"): - self.projectNameLineEdit.setEnabled(False) - self.projectDescriptionTextEdit.setEnabled(False) - self.projectIsPrivateCheckBox.setEnabled(False) - self.projectOwnerComboBox.setEnabled(False) + if self.current_cloud_project.user_role not in ("admin", "manager"): + self.projectNameLineEdit.setEnabled(False) + self.projectDescriptionTextEdit.setEnabled(False) - self.network_manager.projects_cache.get_project_files( - self.current_cloud_project.id - ) + self.network_manager.projects_cache.get_project_files( + self.current_cloud_project.id + ) + + def show_create_project(self): + self.projectsTable.clearSelection() + self.projectsStack.setCurrentWidget(self.projectCreatePage) + self.createProjectWidget.restart() def on_back_button_clicked(self) -> None: self.projectsStack.setCurrentWidget(self.projectsListPage) def on_submit_button_clicked(self) -> None: + assert self.current_cloud_project + cloud_project_data = { "name": self.projectNameLineEdit.text(), "description": self.projectDescriptionTextEdit.toPlainText(), - "owner": self.projectOwnerComboBox.currentData(), - "private": self.projectIsPrivateCheckBox.isChecked(), "local_dir": self.localDirLineEdit.text(), } @@ -1000,61 +867,28 @@ def on_submit_button_clicked(self) -> None: self.projectsFormPage.setEnabled(False) self.feedbackLabel.setVisible(True) - if self.current_cloud_project is None: - self.feedbackLabel.setText(self.tr("Creating project…")) - reply = self.network_manager.create_project( - cloud_project_data["name"], - cloud_project_data["owner"], - cloud_project_data["description"], - cloud_project_data["private"], - ) - reply.finished.connect( - lambda: self.on_create_project_finished( - reply, local_dir=cloud_project_data["local_dir"] - ) - ) - else: - self.current_cloud_project.update_data(cloud_project_data) - self.feedbackLabel.setText(self.tr("Updating project…")) - - reply = self.network_manager.update_project( - self.current_cloud_project.id, - self.current_cloud_project.name, - self.current_cloud_project.owner, - self.current_cloud_project.description, - self.current_cloud_project.is_private, - ) - reply.finished.connect(lambda: self.on_update_project_finished(reply)) + self.current_cloud_project.update_data(cloud_project_data) + self.feedbackLabel.setText(self.tr("Updating project…")) - def on_create_project_finished( - self, reply: QNetworkReply, local_dir: str = None - ) -> None: - self.projectsFormPage.setEnabled(True) + reply = self.network_manager.update_project( + self.current_cloud_project.id, + self.current_cloud_project.name, + self.current_cloud_project.description, + ) + reply.finished.connect(lambda: self.on_update_project_finished(reply)) - try: - payload = self.network_manager.json_object(reply) - except CloudException as err: - self.feedbackLabel.setText("Project create failed: {}".format(str(err))) - self.feedbackLabel.setVisible(True) - return + def on_edit_online_button_clicked(self) -> None: + assert self.current_cloud_project - # save `local_dir` configuration permanently, `CloudProject` constructor does this for free - project = CloudProject( - { - **payload, - "local_dir": local_dir, - } + QDesktopServices.openUrl( + QUrl(self.network_manager.url + self.current_cloud_project.url) ) + def on_create_project_finished(self) -> None: self.projectsStack.setCurrentWidget(self.projectsListPage) - self.feedbackLabel.setVisible(False) - reply = self.network_manager.projects_cache.refresh() - reply.finished.connect( - lambda: self.on_create_project_finished_projects_refreshed( - reply, project.id - ) - ) + def on_create_project_canceled(self) -> None: + self.projectsStack.setCurrentWidget(self.projectsListPage) def update_welcome_label(self) -> None: if self.network_manager.has_token(): @@ -1069,7 +903,7 @@ def update_welcome_label(self) -> None: self.welcomeLabel.setText( self.tr("Greetings {}.").format( - self.network_manager.auth().config("username") + f'{self.network_manager.auth().config("username")}' ) ) @@ -1093,9 +927,11 @@ def update_ui_state(self) -> None: self.network_manager.projects_cache.currently_open_project or self.network_manager.projects_cache.is_currently_open_project_cloud_local ): - self.convertButton.setEnabled(False) + pass + # self.convertButton.setEnabled(False) else: - self.convertButton.setEnabled(True) + pass + # self.convertButton.setEnabled(True) def update_project_table_selection(self) -> None: font = QFont() @@ -1118,27 +954,13 @@ def update_project_table_selection(self) -> None: self.projectsTable.selectionModel().select( index, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows ) + self.projectsTable.scrollToItem( + self.projectsTable.item(row_idx, 0), + QAbstractItemView.EnsureVisible, + ) self.update_project_buttons() - def on_create_project_finished_projects_refreshed( - self, reply: QNetworkReply, project_id: str - ) -> None: - error = from_reply(reply) - - if error: - iface.messageBar().pushWarning( - "QFieldSync", - self.tr("Failed to refresh the project list, please do it manually."), - ) - return - - cloud_project = self.network_manager.projects_cache.find_project(project_id) - self.current_cloud_project = cloud_project - - self.launch() - self.sync() - def on_update_project_finished(self, reply: QNetworkReply) -> None: self.projectsFormPage.setEnabled(True) diff --git a/qfieldsync/qfield_sync.py b/qfieldsync/qfield_sync.py index d535e463..dbd88274 100644 --- a/qfieldsync/qfield_sync.py +++ b/qfieldsync/qfield_sync.py @@ -35,7 +35,6 @@ QFieldCloudItemGuiProvider, QFieldCloudItemProvider, ) -from qfieldsync.gui.cloud_converter_dialog import CloudConverterDialog from qfieldsync.gui.cloud_login_dialog import CloudLoginDialog from qfieldsync.gui.cloud_projects_dialog import CloudProjectsDialog from qfieldsync.gui.cloud_transfer_dialog import CloudTransferDialog @@ -148,9 +147,9 @@ def __init__(self, iface): # autologin if self.preferences.value("qfieldCloudRememberMe"): - dialog = CloudLoginDialog(self.network_manager) - dialog.authenticate() - dialog.hide() + CloudLoginDialog.show_auth_dialog( + self.network_manager, parent=self.iface.mainWindow() + ) # noinspection PyMethodMayBeStatic def tr(self, message): @@ -256,18 +255,6 @@ def initGui(self): parent=self.iface.mainWindow(), ) - self.cloud_convert_action = self.add_action( - QIcon( - os.path.join( - os.path.dirname(__file__), "resources/cloud_convert_project.svg" - ) - ), - text=self.tr("Convert Current Project to Cloud Project"), - callback=self.open_cloud_convert_dialog, - parent=self.iface.mainWindow(), - add_to_toolbar=False, - ) - self.cloud_synchronize_action = self.add_action( QIcon( os.path.join( @@ -368,38 +355,6 @@ def show_synchronize_dialog(self): ) dlg.show() - def open_cloud_convert_dialog(self): - if not self.network_manager.has_token(): - CloudLoginDialog.show_auth_dialog( - self.network_manager, lambda: self.show_cloud_convert_dialog() - ) - else: - self.show_cloud_convert_dialog() - - def show_cloud_convert_dialog(self): - """ - Convert to cloud project. - """ - if QgsProject.instance().mapLayers(): - self.cloud_convert_dlg = CloudConverterDialog( - self.iface, - self.network_manager, - QgsProject.instance(), - self.iface.mainWindow(), - ) - self.cloud_convert_dlg.setAttribute(Qt.WA_DeleteOnClose) - self.cloud_convert_dlg.setWindowFlags( - self.cloud_convert_dlg.windowFlags() | Qt.Tool - ) - self.cloud_convert_dlg.show() - self.cloud_convert_dlg.finished.connect(self.update_button_enabled_status) - else: - self.iface.messageBar().pushMessage( - self.tr("At least one layer is required to convert a project."), - Qgis.Warning, - 5, - ) - def open_cloud_synchronize_dialog(self): if not self.network_manager.has_token(): CloudLoginDialog.show_auth_dialog( @@ -505,10 +460,8 @@ def update_button_enabled_status(self): Will update the plugin buttons according to open dialog and project properties. """ if self.network_manager.projects_cache.is_currently_open_project_cloud_local: - self.cloud_convert_action.setEnabled(False) self.cloud_synchronize_action.setEnabled(True) else: - self.cloud_convert_action.setEnabled(True) self.cloud_synchronize_action.setEnabled(False) try: diff --git a/qfieldsync/resources/cloud_convert_project.svg b/qfieldsync/resources/cloud_create.svg similarity index 100% rename from qfieldsync/resources/cloud_convert_project.svg rename to qfieldsync/resources/cloud_create.svg diff --git a/qfieldsync/ui/cloud_converter_dialog.ui b/qfieldsync/ui/cloud_converter_dialog.ui deleted file mode 100644 index 263d2558..00000000 --- a/qfieldsync/ui/cloud_converter_dialog.ui +++ /dev/null @@ -1,218 +0,0 @@ - - - QFieldCloudConverterDialogBase - - - - 0 - 0 - 650 - 525 - - - - Convert to QFieldCloud Project - - - - - - true - - - You are about to convert a pre-existing project into a QFieldCloud-compatible 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. - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Close|QDialogButtonBox::Save - - - - - - - Project Details - - - - - - Project name - - - - - - - - 0 - 0 - - - - - - - - - - - Export directory - - - - - - - - - - - - ... - - - - - - - - - Qt::Vertical - - - QSizePolicy::Expanding - - - - 16 - 16 - - - - - - - - - - - Information - - - - - - - 0 - 0 - - - - The current project relies on datasets stored in localized data paths, make sure to copy the relevant datasets into the localized data path of devices running QField. On most devices, the path is <u>/QField/basemaps.</u> - - - true - - - - - - - - 0 - 0 - - - - - - - true - - - - - - - - - - Progress - - - - - - Conversion - - - - - - - 0 - - - - - - - Upload to QFieldCloud - - - - - - - 0 - - - - - - - Qt::Vertical - - - QSizePolicy::Expanding - - - - 16 - 16 - - - - - - - - - - - - - button_box - rejected() - QFieldCloudConverterDialogBase - reject() - - - 20 - 20 - - - 20 - 20 - - - - - diff --git a/qfieldsync/ui/cloud_create_project_widget.ui b/qfieldsync/ui/cloud_create_project_widget.ui new file mode 100644 index 00000000..b2487082 --- /dev/null +++ b/qfieldsync/ui/cloud_create_project_widget.ui @@ -0,0 +1,388 @@ + + + CloudCreateProjectWidgetBase + + + + 0 + 0 + 650 + 548 + + + + Convert to QFieldCloud Project + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + + + Choose how to create a new project + + + + + + + 75 + 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 + + + + + + + + 75 + 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. + + + true + + + + + + + + + + + + Cancel + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Next + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Project Details + + + + + + Project description + + + + + + + + + ... + + + + + + + + + + + + Local directory + + + + + + + Project name + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + Information + + + + + + + 0 + 0 + + + + The current project relies on datasets stored in localized data paths, make sure to copy the relevant datasets into the localized data path of devices running QField. On most devices, the path is <u>/QField/basemaps.</u> + + + true + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + + + + + + Back + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Create + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 16 + 16 + + + + + + + + + + + + Progress + + + + + + Conversion + + + + + + + 0 + + + + + + + Upload to QFieldCloud + + + + + + + 0 + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 16 + 16 + + + + + + + + + + + + + + true + + + + + + + + + + + + diff --git a/qfieldsync/ui/cloud_projects_dialog.ui b/qfieldsync/ui/cloud_projects_dialog.ui index f0da78d1..40eb7536 100644 --- a/qfieldsync/ui/cloud_projects_dialog.ui +++ b/qfieldsync/ui/cloud_projects_dialog.ui @@ -21,6 +21,9 @@ + + true + @@ -91,7 +94,7 @@ Qt::ImhDialableCharactersOnly - 0 + 1 @@ -164,6 +167,27 @@ + + + + Create New Project + + + + + + + ../resources/cloud_create.svg../resources/cloud_create.svg + + + + + + + Qt::Vertical + + + @@ -249,39 +273,6 @@ - - - - - 75 - true - - - - New Cloud Project - - - - - - - Create Blank QFieldCloud Project - - - Create blank cloud project - - - - - - - Convert Currently Open Project to Cloud Project - - - Convert currently open project to cloud project - - - @@ -312,7 +303,7 @@ Back - + :/images/themes/default/mActionArrowLeft.svg:/images/themes/default/mActionArrowLeft.svg @@ -361,24 +352,28 @@ Online Project Settings - - - - - - - A valid project name consists of at least 3 characters, starting with a letter and containing nothing other than letters, digits, dashes and underscores + + + + Description - + - Description + Name - + + + + Owner + + + + 0 @@ -387,66 +382,34 @@ 0 - - - Private - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Owner - - - - - - - - - - 0 - 0 - + + + A valid project name consists of at least 3 characters, starting with a letter and containing nothing other than letters, digits, dashes and underscores - - - Refresh the list of available project owners - + - Back - - - - ../resources/refresh.svg../resources/refresh.svg + Private - - - - Name + + + + + + + false + + + A valid project name consists of at least 3 characters, starting with a letter and containing nothing other than letters, digits, dashes and underscores + + + true @@ -504,10 +467,17 @@ + + + + Edit on QFieldCloud + + + - Submit + Update Project Details @@ -697,6 +667,25 @@ + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + diff --git a/qfieldsync/utils/cloud_utils.py b/qfieldsync/utils/cloud_utils.py index fffd9851..8fbbc469 100644 --- a/qfieldsync/utils/cloud_utils.py +++ b/qfieldsync/utils/cloud_utils.py @@ -21,6 +21,19 @@ import re +from enum import Enum +from pathlib import Path +from typing import Tuple + +from qgis.PyQt.QtCore import QObject + +from qfieldsync.libqfieldsync.utils.qgis import get_qgis_files_within_dir + + +class LocalDirFeedback(Enum): + Error = "error" + Warning = "warning" + Success = "success" def to_cloud_title(title): @@ -35,3 +48,49 @@ def call(*args, **kwargs): return call return wrapper + + +def local_dir_feedback( + local_dir: str, + no_path_status: LocalDirFeedback = LocalDirFeedback.Error, + not_dir_status: LocalDirFeedback = LocalDirFeedback.Error, + not_existing_status: LocalDirFeedback = LocalDirFeedback.Warning, + no_project_status: LocalDirFeedback = LocalDirFeedback.Success, + single_project_status: LocalDirFeedback = LocalDirFeedback.Success, + multiple_projects_status: LocalDirFeedback = LocalDirFeedback.Error, +) -> Tuple[LocalDirFeedback, str]: + dummy = QObject() + if not local_dir: + return no_path_status, dummy.tr( + "Please select local directory where the project to be stored." + ) + elif Path(local_dir).exists() and not Path(local_dir).is_dir(): + return not_dir_status, dummy.tr( + "The entered path is not an directory. Please enter a valid directory path." + ) + elif not Path(local_dir).exists(): + return not_existing_status, dummy.tr( + "The entered path is not an existing directory. It will be created after you submit this form." + ) + elif len(get_qgis_files_within_dir(Path(local_dir))) == 0: + message = dummy.tr("The entered path does not contain a QGIS project file yet.") + status = no_project_status + + if single_project_status is not LocalDirFeedback.Success: + message += " " + message += dummy.tr("You can always add one later.") + + return status, message + elif len(get_qgis_files_within_dir(Path(local_dir))) == 1: + message = dummy.tr("The entered path contains one QGIS project file.") + status = single_project_status + + if single_project_status is not LocalDirFeedback.Success: + message += " " + message += dummy.tr("Exactly as it should be.") + + return status, message + else: + return multiple_projects_status, dummy.tr( + "Multiple project files have been found in the directory. Please leave exactly one QGIS project in the root directory." + )