From b36fe780f0fb6fdfa7d06b9acc9fdb80b1aca5b6 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Thu, 21 Dec 2023 16:07:29 +0700 Subject: [PATCH 1/8] Introduce an oudated state for cloud project through local last update vs. remote last update check --- src/core/qfieldcloudprojectsmodel.cpp | 24 ++++++++++++++++++------ src/core/qfieldcloudprojectsmodel.h | 10 +++++++--- src/qml/QFieldCloudScreen.qml | 4 ++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/core/qfieldcloudprojectsmodel.cpp b/src/core/qfieldcloudprojectsmodel.cpp index 9a8b3051c1..3fd760b09c 100644 --- a/src/core/qfieldcloudprojectsmodel.cpp +++ b/src/core/qfieldcloudprojectsmodel.cpp @@ -481,13 +481,14 @@ void QFieldCloudProjectsModel::projectRefreshData( const QString &projectId, con project->isPrivate = projectData.value( "is_public" ).isUndefined() ? projectData.value( "private" ).toBool() : !projectData.value( "is_public" ).toBool( false ); project->canRepackage = projectData.value( "can_repackage" ).toBool(); project->needsRepackaging = projectData.value( "needs_repackaging" ).toBool(); + project->updatedAt = QDateTime::fromString( projectData.value( "data_last_updated_at" ).toString(), Qt::ISODate ); + project->isOutdated = project->lastLocalUpdatedAt.isValid() ? project->updatedAt > project->lastLocalUpdatedAt : false; project->lastRefreshedAt = QDateTime::currentDateTimeUtc(); QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "name" ), project->name ); QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "owner" ), project->owner ); QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "description" ), project->description ); QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "userRole" ), project->userRole ); - QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "updatedAt" ), project->updatedAt ); QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "isPrivate" ), project->isPrivate ); QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "canRepackage" ), project->canRepackage ); QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "needsRepackaging" ), project->needsRepackaging ); @@ -1813,12 +1814,16 @@ void QFieldCloudProjectsModel::downloadFileConnections( const QString &projectId project->localPath = QFieldCloudUtils::localProjectFilePath( mUsername, projectId ); project->lastLocalExportedAt = QDateTime::currentDateTimeUtc().toString( Qt::ISODate ); project->lastLocalExportId = QUuid::createUuid().toString( QUuid::WithoutBraces ); + project->lastLocalUpdatedAt = project->updatedAt; + project->isOutdated = false; + QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "lastExportedAt" ), project->lastExportedAt ); QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "lastExportId" ), project->lastExportId ); QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "lastLocalExportedAt" ), project->lastLocalExportedAt ); QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "lastLocalExportId" ), project->lastLocalExportId ); + QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "lastLocalUpdatedAt" ), project->lastLocalUpdatedAt ); - rolesChanged << StatusRole << LocalPathRole << CheckoutRole << LastLocalExportedAtRole; + rolesChanged << StatusRole << OutdatedRole << LocalPathRole << CheckoutRole << LastLocalExportedAtRole; emit dataChanged( projectIndex, projectIndex, rolesChanged ); emit projectDownloadFinished( projectId ); @@ -1842,6 +1847,7 @@ QHash QFieldCloudProjectsModel::roleNames() const roles[ModificationRole] = "Modification"; roles[CheckoutRole] = "Checkout"; roles[StatusRole] = "Status"; + roles[OutdatedRole] = "Oudated"; roles[ErrorStatusRole] = "ErrorStatus"; roles[ErrorStringRole] = "ErrorString"; roles[DownloadProgressRole] = "DownloadProgress"; @@ -1875,6 +1881,12 @@ void QFieldCloudProjectsModel::reload( const QJsonArray &remoteProjects ) cloudProject->lastLocalExportId = QFieldCloudUtils::projectSetting( cloudProject->id, QStringLiteral( "lastLocalExportId" ) ).toString(); cloudProject->lastLocalExportedAt = QFieldCloudUtils::projectSetting( cloudProject->id, QStringLiteral( "lastLocalExportedAt" ) ).toString(); cloudProject->lastLocalPushDeltas = QFieldCloudUtils::projectSetting( cloudProject->id, QStringLiteral( "lastLocalPushDeltas" ) ).toString(); + cloudProject->lastLocalUpdatedAt = QFieldCloudUtils::projectSetting( cloudProject->id, QStringLiteral( "lastLocalUpdatedAt" ) ).toDateTime(); + + if ( cloudProject->updatedAt > cloudProject->lastLocalUpdatedAt ) + { + cloudProject->isOutdated = true; + } // generate local export id if not present. Possible reasons for missing localExportId are: // - just upgraded QField that introduced the field @@ -1895,17 +1907,15 @@ void QFieldCloudProjectsModel::reload( const QJsonArray &remoteProjects ) projectDetails.value( "name" ).toString(), projectDetails.value( "description" ).toString(), projectDetails.value( "user_role" ).toString(), - QString(), RemoteCheckout, ProjectStatus::Idle, + QDateTime::fromString( projectDetails.value( "data_last_updated_at" ).toString(), Qt::ISODate ), projectDetails.value( "can_repackage" ).toBool(), projectDetails.value( "needs_repackaging" ).toBool() ); - const QString projectPrefix = QStringLiteral( "QFieldCloud/projects/%1" ).arg( cloudProject->id ); QFieldCloudUtils::setProjectSetting( cloudProject->id, QStringLiteral( "owner" ), cloudProject->owner ); QFieldCloudUtils::setProjectSetting( cloudProject->id, QStringLiteral( "name" ), cloudProject->name ); QFieldCloudUtils::setProjectSetting( cloudProject->id, QStringLiteral( "description" ), cloudProject->description ); - QFieldCloudUtils::setProjectSetting( cloudProject->id, QStringLiteral( "updatedAt" ), cloudProject->updatedAt ); QFieldCloudUtils::setProjectSetting( cloudProject->id, QStringLiteral( "userRole" ), cloudProject->userRole ); QFieldCloudUtils::setProjectSetting( cloudProject->id, QStringLiteral( "canRepackage" ), cloudProject->canRepackage ); QFieldCloudUtils::setProjectSetting( cloudProject->id, QStringLiteral( "needsRepackaging" ), cloudProject->needsRepackaging ); @@ -1956,7 +1966,7 @@ void QFieldCloudProjectsModel::reload( const QJsonArray &remoteProjects ) const QString updatedAt = QFieldCloudUtils::projectSetting( projectId, QStringLiteral( "updatedAt" ) ).toString(); const QString userRole = QFieldCloudUtils::projectSetting( projectId, QStringLiteral( "userRole" ) ).toString(); - CloudProject *cloudProject = new CloudProject( projectId, true, owner, name, description, userRole, QString(), LocalCheckout, ProjectStatus::Idle, false, false ); + CloudProject *cloudProject = new CloudProject( projectId, true, owner, name, description, userRole, LocalCheckout, ProjectStatus::Idle, QDateTime(), false, false ); cloudProject->localPath = QFieldCloudUtils::localProjectFilePath( username, cloudProject->id ); QDir localPath( QStringLiteral( "%1/%2/%3" ).arg( QFieldCloudUtils::localCloudDirectory(), username, cloudProject->id ) ); @@ -2007,6 +2017,8 @@ QVariant QFieldCloudProjectsModel::data( const QModelIndex &index, int role ) co return static_cast( mProjects.at( index.row() )->checkout ); case StatusRole: return static_cast( mProjects.at( index.row() )->status ); + case OutdatedRole: + return mProjects.at( index.row() )->isOutdated; case ErrorStatusRole: return static_cast( mProjects.at( index.row() )->errorStatus ); case ErrorStringRole: diff --git a/src/core/qfieldcloudprojectsmodel.h b/src/core/qfieldcloudprojectsmodel.h index da2e215d12..2c69e3939c 100644 --- a/src/core/qfieldcloudprojectsmodel.h +++ b/src/core/qfieldcloudprojectsmodel.h @@ -49,6 +49,7 @@ class QFieldCloudProjectsModel : public QAbstractListModel ModificationRole, CheckoutRole, StatusRole, + OutdatedRole, ErrorStatusRole, ErrorStringRole, DownloadProgressRole, @@ -350,9 +351,9 @@ class QFieldCloudProjectsModel : public QAbstractListModel const QString &name, const QString &description, const QString &userRole, - const QString &updatedAt, const ProjectCheckouts &checkout, const ProjectStatus &status, + const QDateTime &updatedAt, bool canRepackage, bool needsRepackaging ) : id( id ) @@ -361,9 +362,9 @@ class QFieldCloudProjectsModel : public QAbstractListModel , name( name ) , description( description ) , userRole( userRole ) - , updatedAt( updatedAt ) , checkout( checkout ) , status( status ) + , updatedAt( updatedAt ) , canRepackage( canRepackage ) , needsRepackaging( needsRepackaging ) {} @@ -377,12 +378,12 @@ class QFieldCloudProjectsModel : public QAbstractListModel QString name; QString description; QString userRole; - QString updatedAt; ProjectErrorStatus errorStatus = ProjectErrorStatus::NoErrorStatus; ProjectCheckouts checkout; ProjectStatus status; bool canRepackage = false; bool needsRepackaging = false; + bool isOutdated = false; ProjectModifications modification = ProjectModification::NoModification; QString localPath; @@ -414,6 +415,9 @@ class QFieldCloudProjectsModel : public QAbstractListModel QString lastLocalExportId; QString lastLocalPushDeltas; + QDateTime updatedAt; + QDateTime lastLocalUpdatedAt; + QDateTime lastRefreshedAt; QMap jobs; }; diff --git a/src/qml/QFieldCloudScreen.qml b/src/qml/QFieldCloudScreen.qml index 4e8565f351..8b96ae1b64 100644 --- a/src/qml/QFieldCloudScreen.qml +++ b/src/qml/QFieldCloudScreen.qml @@ -391,6 +391,10 @@ Page { default: break } + + if (Oudated) { + status += qsTr( ', oudated project data'); + } } var localChanges = ( LocalDeltasCount > 0 ) ? qsTr('Has changes. ') : '' From 7f354fa23d4848ee5d43d79679fdbdbf46a121b4 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Thu, 21 Dec 2023 17:07:19 +0700 Subject: [PATCH 2/8] When uploading deltas, do our best to avoid being outdated on self-changes --- src/core/qfieldcloudprojectsmodel.cpp | 23 +++++++++++++++++++++-- src/core/qfieldcloudprojectsmodel.h | 1 + src/qml/QFieldCloudScreen.qml | 7 +++---- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/core/qfieldcloudprojectsmodel.cpp b/src/core/qfieldcloudprojectsmodel.cpp index 3fd760b09c..e0f20cf7e8 100644 --- a/src/core/qfieldcloudprojectsmodel.cpp +++ b/src/core/qfieldcloudprojectsmodel.cpp @@ -481,9 +481,23 @@ void QFieldCloudProjectsModel::projectRefreshData( const QString &projectId, con project->isPrivate = projectData.value( "is_public" ).isUndefined() ? projectData.value( "private" ).toBool() : !projectData.value( "is_public" ).toBool( false ); project->canRepackage = projectData.value( "can_repackage" ).toBool(); project->needsRepackaging = projectData.value( "needs_repackaging" ).toBool(); - project->updatedAt = QDateTime::fromString( projectData.value( "data_last_updated_at" ).toString(), Qt::ISODate ); - project->isOutdated = project->lastLocalUpdatedAt.isValid() ? project->updatedAt > project->lastLocalUpdatedAt : false; project->lastRefreshedAt = QDateTime::currentDateTimeUtc(); + project->updatedAt = QDateTime::fromString( projectData.value( "data_last_updated_at" ).toString(), Qt::ISODate ); + + if ( !project->isOutdated && refreshReason == ProjectRefreshReason::DeltaUploaded ) + { + // When pushing deltas to the cloud, the server hasn't had the time to refresh + // its last updated at value; we therefore consider an arbitrary last updated at + // value to be an hour from now. This is to avoid subsequently telling users + // they have an oudated project when the only thing that changed is the delta(s) + // they pushed. + project->lastLocalUpdatedAt = project->lastRefreshedAt.addSecs( 60 * 60 ); + QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "lastLocalUpdatedAt" ), project->lastLocalUpdatedAt ); + } + else + { + project->isOutdated = project->lastLocalUpdatedAt.isValid() ? project->updatedAt > project->lastLocalUpdatedAt : false; + } QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "name" ), project->name ); QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "owner" ), project->owner ); @@ -1332,6 +1346,7 @@ void QFieldCloudProjectsModel::projectUpload( const QString &projectId, const bo emit dataChanged( projectIndex, projectIndex, QVector() << StatusRole ); emit pushFinished( projectId, false ); + projectRefreshData( projectId, ProjectRefreshReason::DeltaUploaded ); } } ); @@ -1396,6 +1411,7 @@ void QFieldCloudProjectsModel::projectUpload( const QString &projectId, const bo { emit dataChanged( projectIndex, projectIndex, QVector() << StatusRole ); emit pushFinished( projectId, false ); + projectRefreshData( projectId, ProjectRefreshReason::DeltaUploaded ); } } } ); @@ -1883,6 +1899,9 @@ void QFieldCloudProjectsModel::reload( const QJsonArray &remoteProjects ) cloudProject->lastLocalPushDeltas = QFieldCloudUtils::projectSetting( cloudProject->id, QStringLiteral( "lastLocalPushDeltas" ) ).toString(); cloudProject->lastLocalUpdatedAt = QFieldCloudUtils::projectSetting( cloudProject->id, QStringLiteral( "lastLocalUpdatedAt" ) ).toDateTime(); + qDebug() << cloudProject->name; + qDebug() << cloudProject->updatedAt; + qDebug() << cloudProject->lastLocalUpdatedAt; if ( cloudProject->updatedAt > cloudProject->lastLocalUpdatedAt ) { cloudProject->isOutdated = true; diff --git a/src/core/qfieldcloudprojectsmodel.h b/src/core/qfieldcloudprojectsmodel.h index 2c69e3939c..e24f957a2d 100644 --- a/src/core/qfieldcloudprojectsmodel.h +++ b/src/core/qfieldcloudprojectsmodel.h @@ -165,6 +165,7 @@ class QFieldCloudProjectsModel : public QAbstractListModel enum class ProjectRefreshReason { Package, + DeltaUploaded }; Q_ENUM( ProjectRefreshReason ) diff --git a/src/qml/QFieldCloudScreen.qml b/src/qml/QFieldCloudScreen.qml index 8b96ae1b64..a3c2687674 100644 --- a/src/qml/QFieldCloudScreen.qml +++ b/src/qml/QFieldCloudScreen.qml @@ -387,14 +387,13 @@ Page { break case QFieldCloudProjectsModel.LocalAndRemoteCheckout: status = qsTr( 'Available locally' ) + if (Oudated) { + status += qsTr( ', updated data available on the cloud'); + } break default: break } - - if (Oudated) { - status += qsTr( ', oudated project data'); - } } var localChanges = ( LocalDeltasCount > 0 ) ? qsTr('Has changes. ') : '' From f6b2c906cb57904fcb0505a9371916a19a2a91be Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Thu, 21 Dec 2023 17:26:32 +0700 Subject: [PATCH 3/8] Fix cppcheck warning --- src/core/qfieldcloudprojectsmodel.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/qfieldcloudprojectsmodel.h b/src/core/qfieldcloudprojectsmodel.h index e24f957a2d..bc62994850 100644 --- a/src/core/qfieldcloudprojectsmodel.h +++ b/src/core/qfieldcloudprojectsmodel.h @@ -382,6 +382,7 @@ class QFieldCloudProjectsModel : public QAbstractListModel ProjectErrorStatus errorStatus = ProjectErrorStatus::NoErrorStatus; ProjectCheckouts checkout; ProjectStatus status; + QDateTime updatedAt; bool canRepackage = false; bool needsRepackaging = false; bool isOutdated = false; @@ -415,8 +416,6 @@ class QFieldCloudProjectsModel : public QAbstractListModel QString lastLocalExportedAt; QString lastLocalExportId; QString lastLocalPushDeltas; - - QDateTime updatedAt; QDateTime lastLocalUpdatedAt; QDateTime lastRefreshedAt; From e5cca1d04da721099a8fb0f77991a6efe4958186 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Thu, 21 Dec 2023 17:42:44 +0700 Subject: [PATCH 4/8] Fix typo, remove debug code, warn users of new data on cloud when opening a cloud project --- src/core/qfieldcloudprojectsmodel.cpp | 12 +++++++----- src/qml/QFieldCloudScreen.qml | 2 +- src/qml/qgismobileapp.qml | 6 ++++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/core/qfieldcloudprojectsmodel.cpp b/src/core/qfieldcloudprojectsmodel.cpp index e0f20cf7e8..3a0e9128b6 100644 --- a/src/core/qfieldcloudprojectsmodel.cpp +++ b/src/core/qfieldcloudprojectsmodel.cpp @@ -489,7 +489,7 @@ void QFieldCloudProjectsModel::projectRefreshData( const QString &projectId, con // When pushing deltas to the cloud, the server hasn't had the time to refresh // its last updated at value; we therefore consider an arbitrary last updated at // value to be an hour from now. This is to avoid subsequently telling users - // they have an oudated project when the only thing that changed is the delta(s) + // they have an outdated project when the only thing that changed is the delta(s) // they pushed. project->lastLocalUpdatedAt = project->lastRefreshedAt.addSecs( 60 * 60 ); QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "lastLocalUpdatedAt" ), project->lastLocalUpdatedAt ); @@ -509,6 +509,11 @@ void QFieldCloudProjectsModel::projectRefreshData( const QString &projectId, con emit dataChanged( projectIndex, projectIndex, QVector::fromList( roleNames().keys() ) ); emit projectRefreshed( projectId, refreshReason ); + + if ( mCurrentProjectId == projectId ) + { + emit currentProjectDataChanged(); + } } ); } @@ -1863,7 +1868,7 @@ QHash QFieldCloudProjectsModel::roleNames() const roles[ModificationRole] = "Modification"; roles[CheckoutRole] = "Checkout"; roles[StatusRole] = "Status"; - roles[OutdatedRole] = "Oudated"; + roles[OutdatedRole] = "Outdated"; roles[ErrorStatusRole] = "ErrorStatus"; roles[ErrorStringRole] = "ErrorString"; roles[DownloadProgressRole] = "DownloadProgress"; @@ -1899,9 +1904,6 @@ void QFieldCloudProjectsModel::reload( const QJsonArray &remoteProjects ) cloudProject->lastLocalPushDeltas = QFieldCloudUtils::projectSetting( cloudProject->id, QStringLiteral( "lastLocalPushDeltas" ) ).toString(); cloudProject->lastLocalUpdatedAt = QFieldCloudUtils::projectSetting( cloudProject->id, QStringLiteral( "lastLocalUpdatedAt" ) ).toDateTime(); - qDebug() << cloudProject->name; - qDebug() << cloudProject->updatedAt; - qDebug() << cloudProject->lastLocalUpdatedAt; if ( cloudProject->updatedAt > cloudProject->lastLocalUpdatedAt ) { cloudProject->isOutdated = true; diff --git a/src/qml/QFieldCloudScreen.qml b/src/qml/QFieldCloudScreen.qml index a3c2687674..b9641fca68 100644 --- a/src/qml/QFieldCloudScreen.qml +++ b/src/qml/QFieldCloudScreen.qml @@ -387,7 +387,7 @@ Page { break case QFieldCloudProjectsModel.LocalAndRemoteCheckout: status = qsTr( 'Available locally' ) - if (Oudated) { + if (Outdated) { status += qsTr( ', updated data available on the cloud'); } break diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index abd9e4d1de..809d59e5c0 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -3352,6 +3352,12 @@ ApplicationWindow { layoutListInstantiator.model.reloadModel() settings.setValue( "/QField/FirstRunFlag", false ) + + console.log(cloudProjectsModel.currentProjectData.Name) + console.log(cloudProjectsModel.currentProjectData.Outdated) + if (cloudProjectsModel.currentProjectData.Outdated) { + displayToast(qsTr('This project has updated data on the cloud, you should synchronize.')) + } } function onSetMapExtent(extent) { From c3aa7648e15092821e335426ab26416231691575 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Thu, 21 Dec 2023 18:04:45 +0700 Subject: [PATCH 5/8] Use an attention cloud icon in the dashboard and add a couple of toaster messages to draw attention to outdated status --- images/images.qrc | 1 + images/themes/qfield/nodpi/ic_cloud_attention_24dp.svg | 2 ++ src/qml/DashBoard.qml | 2 +- src/qml/QFieldCloudPopup.qml | 8 ++++++-- 4 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 images/themes/qfield/nodpi/ic_cloud_attention_24dp.svg diff --git a/images/images.qrc b/images/images.qrc index 8cca73d4b7..74db3b7e66 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -174,6 +174,7 @@ themes/qfield/xxxhdpi/ic_close_black_24dp.png themes/qfield/nodpi/ic_cloud_24dp.svg themes/qfield/nodpi/ic_cloud_active_24dp.svg + themes/qfield/nodpi/ic_cloud_attention_24dp.svg themes/qfield/nodpi/ic_cloud_project_48dp.svg themes/qfield/hdpi/ic_cloud_project_48dp.png themes/qfield/mdpi/ic_cloud_project_48dp.png diff --git a/images/themes/qfield/nodpi/ic_cloud_attention_24dp.svg b/images/themes/qfield/nodpi/ic_cloud_attention_24dp.svg new file mode 100644 index 0000000000..7c79648e4c --- /dev/null +++ b/images/themes/qfield/nodpi/ic_cloud_attention_24dp.svg @@ -0,0 +1,2 @@ + +image/svg+xml diff --git a/src/qml/DashBoard.qml b/src/qml/DashBoard.qml index fb03b02431..98a4465239 100644 --- a/src/qml/DashBoard.qml +++ b/src/qml/DashBoard.qml @@ -113,7 +113,7 @@ Drawer { return Theme.getThemeVectorIcon('ic_cloud_active_24dp'); } case QFieldCloudProjectsModel.Idle: - return Theme.getThemeVectorIcon('ic_cloud_active_24dp'); + return cloudProjectsModel.currentProjectData.Outdated ? Theme.getThemeVectorIcon('ic_cloud_attention_24dp') : Theme.getThemeVectorIcon('ic_cloud_active_24dp'); default: Theme.getThemeVectorIcon( 'ic_cloud_24dp' ); } bgcolor: "transparent" diff --git a/src/qml/QFieldCloudPopup.qml b/src/qml/QFieldCloudPopup.qml index 34a32b7017..72e5a65abe 100644 --- a/src/qml/QFieldCloudPopup.qml +++ b/src/qml/QFieldCloudPopup.qml @@ -605,11 +605,15 @@ Popup { function show() { visible = !visible - if ( cloudProjectsModel.currentProjectId && cloudConnection.hasToken && cloudConnection.status === QFieldCloudConnection.Disconnected ) + if ( cloudProjectsModel.currentProjectId && cloudConnection.hasToken && cloudConnection.status === QFieldCloudConnection.Disconnected ) { cloudConnection.login(); + } - if ( cloudConnection.status === QFieldCloudConnection.Connecting ) + if ( cloudConnection.status === QFieldCloudConnection.Connecting ) { displayToast(qsTr('Connecting cloud')) + } else if ( cloudProjectsModel.currentProjectData.Outdated ) { + displayToast(qsTr('This project has updated data on the cloud, you should synchronize.')) + } } function projectUpload(shouldDownloadUpdates) { From d2ad2955656a6c0be0ebf6aa15ace66e994b533f Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Fri, 22 Dec 2023 08:53:35 +0700 Subject: [PATCH 6/8] Remove console.log, reduce arbitrary time to 30 minutes --- src/core/qfieldcloudprojectsmodel.cpp | 2 +- src/qml/qgismobileapp.qml | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core/qfieldcloudprojectsmodel.cpp b/src/core/qfieldcloudprojectsmodel.cpp index 3a0e9128b6..999810e9c4 100644 --- a/src/core/qfieldcloudprojectsmodel.cpp +++ b/src/core/qfieldcloudprojectsmodel.cpp @@ -491,7 +491,7 @@ void QFieldCloudProjectsModel::projectRefreshData( const QString &projectId, con // value to be an hour from now. This is to avoid subsequently telling users // they have an outdated project when the only thing that changed is the delta(s) // they pushed. - project->lastLocalUpdatedAt = project->lastRefreshedAt.addSecs( 60 * 60 ); + project->lastLocalUpdatedAt = project->lastRefreshedAt.addSecs( 60 * 30 ); QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "lastLocalUpdatedAt" ), project->lastLocalUpdatedAt ); } else diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index 809d59e5c0..4a3fecece8 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -3353,8 +3353,6 @@ ApplicationWindow { settings.setValue( "/QField/FirstRunFlag", false ) - console.log(cloudProjectsModel.currentProjectData.Name) - console.log(cloudProjectsModel.currentProjectData.Outdated) if (cloudProjectsModel.currentProjectData.Outdated) { displayToast(qsTr('This project has updated data on the cloud, you should synchronize.')) } From 1addf902e215d4b681db834b660f0f5f116aec0d Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 23 Dec 2023 12:14:19 +0700 Subject: [PATCH 7/8] Add project file outdated state --- src/core/qfieldcloudprojectsmodel.cpp | 97 +++++++++++++++++++-------- src/core/qfieldcloudprojectsmodel.h | 15 +++-- src/qml/DashBoard.qml | 2 +- src/qml/QFieldCloudPopup.qml | 4 +- src/qml/QFieldCloudScreen.qml | 2 +- src/qml/qgismobileapp.qml | 13 ++-- 6 files changed, 94 insertions(+), 39 deletions(-) diff --git a/src/core/qfieldcloudprojectsmodel.cpp b/src/core/qfieldcloudprojectsmodel.cpp index 999810e9c4..ce09a40557 100644 --- a/src/core/qfieldcloudprojectsmodel.cpp +++ b/src/core/qfieldcloudprojectsmodel.cpp @@ -352,6 +352,60 @@ void QFieldCloudProjectsModel::refreshProjectModification( const QString &projec } } +void QFieldCloudProjectsModel::refreshProjectFileOutdatedStatus( const QString &projectId ) +{ + const QModelIndex projectIndex = findProjectIndex( projectId ); + if ( !projectIndex.isValid() ) + return; + + CloudProject *project = mProjects[projectIndex.row()]; + NetworkReply *reply = mCloudConnection->get( QStringLiteral( "/api/v1/files/%1/" ).arg( projectId ) ); + + connect( reply, &NetworkReply::finished, reply, [=]() { + if ( !findProject( projectId ) ) + { + QgsLogger::debug( QStringLiteral( "Project %1: project has been deleted while refreshing project file outdated status." ).arg( projectId ) ); + return; + } + + QNetworkReply *rawReply = reply->reply(); + reply->deleteLater(); + + if ( rawReply->error() != QNetworkReply::NoError ) + { + QgsLogger::debug( QStringLiteral( "Project %1: failed to refresh the project file outdated satus. %2" ).arg( projectId, QFieldCloudConnection::errorString( rawReply ) ) ); + return; + } + + const QJsonArray files = QJsonDocument::fromJson( rawReply->readAll() ).array(); + const QString lastProjectFileSha256 = QFieldCloudUtils::projectSetting( project->id, QStringLiteral( "lastProjectFileSha256" ), QString() ).toString(); + for ( const auto file : files ) + { + QVariantHash fileDetails = file.toObject().toVariantHash(); + const QString fileName = fileDetails.value( "name" ).toString().toLower(); + if ( fileName.endsWith( QStringLiteral( ".qgs" ) ) || fileName.endsWith( QStringLiteral( ".qgz" ) ) ) + { + if ( lastProjectFileSha256.isEmpty() ) + { + // First check, store for future comparison + QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "lastProjectFileSha256" ), fileDetails.value( "sha256" ).toString() ); + } + else if ( lastProjectFileSha256 != fileDetails.value( "sha256" ).toString() ) + { + project->projectFileIsOutdated = true; + QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "projectFileOudated" ), true ); + emit dataChanged( projectIndex, projectIndex, QVector() << ProjectFileOutdatedRole ); + + if ( mCurrentProjectId == projectId ) + { + emit currentProjectDataChanged(); + } + } + } + } + } ); +} + QString QFieldCloudProjectsModel::layerFileName( const QgsMapLayer *layer ) const { return layer->dataProvider()->dataSourceUri().split( '|' )[0]; @@ -482,22 +536,8 @@ void QFieldCloudProjectsModel::projectRefreshData( const QString &projectId, con project->canRepackage = projectData.value( "can_repackage" ).toBool(); project->needsRepackaging = projectData.value( "needs_repackaging" ).toBool(); project->lastRefreshedAt = QDateTime::currentDateTimeUtc(); - project->updatedAt = QDateTime::fromString( projectData.value( "data_last_updated_at" ).toString(), Qt::ISODate ); - - if ( !project->isOutdated && refreshReason == ProjectRefreshReason::DeltaUploaded ) - { - // When pushing deltas to the cloud, the server hasn't had the time to refresh - // its last updated at value; we therefore consider an arbitrary last updated at - // value to be an hour from now. This is to avoid subsequently telling users - // they have an outdated project when the only thing that changed is the delta(s) - // they pushed. - project->lastLocalUpdatedAt = project->lastRefreshedAt.addSecs( 60 * 30 ); - QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "lastLocalUpdatedAt" ), project->lastLocalUpdatedAt ); - } - else - { - project->isOutdated = project->lastLocalUpdatedAt.isValid() ? project->updatedAt > project->lastLocalUpdatedAt : false; - } + project->dataLastUpdatedAt = QDateTime::fromString( projectData.value( "data_last_updated_at" ).toString(), Qt::ISODate ); + project->isOutdated = project->lastLocalDataLastUpdatedAt.isValid() ? project->dataLastUpdatedAt > project->lastLocalDataLastUpdatedAt : false; QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "name" ), project->name ); QFieldCloudUtils::setProjectSetting( project->id, QStringLiteral( "owner" ), project->owner ); @@ -1835,16 +1875,19 @@ void QFieldCloudProjectsModel::downloadFileConnections( const QString &projectId project->localPath = QFieldCloudUtils::localProjectFilePath( mUsername, projectId ); project->lastLocalExportedAt = QDateTime::currentDateTimeUtc().toString( Qt::ISODate ); project->lastLocalExportId = QUuid::createUuid().toString( QUuid::WithoutBraces ); - project->lastLocalUpdatedAt = project->updatedAt; + project->lastLocalDataLastUpdatedAt = project->dataLastUpdatedAt; project->isOutdated = false; + project->projectFileIsOutdated = false; QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "lastExportedAt" ), project->lastExportedAt ); QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "lastExportId" ), project->lastExportId ); QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "lastLocalExportedAt" ), project->lastLocalExportedAt ); QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "lastLocalExportId" ), project->lastLocalExportId ); - QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "lastLocalUpdatedAt" ), project->lastLocalUpdatedAt ); + QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "lastLocalDataLastUpdatedAt" ), project->lastLocalDataLastUpdatedAt ); + QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "lastProjectFileSha256" ), QString() ); + QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "projectFileOudated" ), false ); - rolesChanged << StatusRole << OutdatedRole << LocalPathRole << CheckoutRole << LastLocalExportedAtRole; + rolesChanged << StatusRole << ProjectOutdatedRole << ProjectFileOutdatedRole << LocalPathRole << CheckoutRole << LastLocalExportedAtRole; emit dataChanged( projectIndex, projectIndex, rolesChanged ); emit projectDownloadFinished( projectId ); @@ -1868,7 +1911,8 @@ QHash QFieldCloudProjectsModel::roleNames() const roles[ModificationRole] = "Modification"; roles[CheckoutRole] = "Checkout"; roles[StatusRole] = "Status"; - roles[OutdatedRole] = "Outdated"; + roles[ProjectOutdatedRole] = "ProjectOutdated"; + roles[ProjectFileOutdatedRole] = "ProjectFileOutdated"; roles[ErrorStatusRole] = "ErrorStatus"; roles[ErrorStringRole] = "ErrorString"; roles[DownloadProgressRole] = "DownloadProgress"; @@ -1902,12 +1946,9 @@ void QFieldCloudProjectsModel::reload( const QJsonArray &remoteProjects ) cloudProject->lastLocalExportId = QFieldCloudUtils::projectSetting( cloudProject->id, QStringLiteral( "lastLocalExportId" ) ).toString(); cloudProject->lastLocalExportedAt = QFieldCloudUtils::projectSetting( cloudProject->id, QStringLiteral( "lastLocalExportedAt" ) ).toString(); cloudProject->lastLocalPushDeltas = QFieldCloudUtils::projectSetting( cloudProject->id, QStringLiteral( "lastLocalPushDeltas" ) ).toString(); - cloudProject->lastLocalUpdatedAt = QFieldCloudUtils::projectSetting( cloudProject->id, QStringLiteral( "lastLocalUpdatedAt" ) ).toDateTime(); - - if ( cloudProject->updatedAt > cloudProject->lastLocalUpdatedAt ) - { - cloudProject->isOutdated = true; - } + cloudProject->lastLocalDataLastUpdatedAt = QFieldCloudUtils::projectSetting( cloudProject->id, QStringLiteral( "lastLocalDataLastUpdatedAt" ) ).toDateTime(); + cloudProject->isOutdated = cloudProject->dataLastUpdatedAt > cloudProject->lastLocalDataLastUpdatedAt; + cloudProject->projectFileIsOutdated = QFieldCloudUtils::projectSetting( cloudProject->id, QStringLiteral( "projectFileOudated" ), false ).toBool(); // generate local export id if not present. Possible reasons for missing localExportId are: // - just upgraded QField that introduced the field @@ -2038,8 +2079,10 @@ QVariant QFieldCloudProjectsModel::data( const QModelIndex &index, int role ) co return static_cast( mProjects.at( index.row() )->checkout ); case StatusRole: return static_cast( mProjects.at( index.row() )->status ); - case OutdatedRole: + case ProjectOutdatedRole: return mProjects.at( index.row() )->isOutdated; + case ProjectFileOutdatedRole: + return mProjects.at( index.row() )->projectFileIsOutdated; case ErrorStatusRole: return static_cast( mProjects.at( index.row() )->errorStatus ); case ErrorStringRole: diff --git a/src/core/qfieldcloudprojectsmodel.h b/src/core/qfieldcloudprojectsmodel.h index bc62994850..d7d36449a1 100644 --- a/src/core/qfieldcloudprojectsmodel.h +++ b/src/core/qfieldcloudprojectsmodel.h @@ -49,7 +49,8 @@ class QFieldCloudProjectsModel : public QAbstractListModel ModificationRole, CheckoutRole, StatusRole, - OutdatedRole, + ProjectOutdatedRole, + ProjectFileOutdatedRole, ErrorStatusRole, ErrorStringRole, DownloadProgressRole, @@ -250,6 +251,9 @@ class QFieldCloudProjectsModel : public QAbstractListModel //! Updates the project modification for given \a projectId. Q_INVOKABLE void refreshProjectModification( const QString &projectId ); + //! Refreshes the project file (.qgs, .qgz) outdated status. + Q_INVOKABLE void refreshProjectFileOutdatedStatus( const QString &projectId ); + //! Returns the model role names. QHash roleNames() const override; @@ -354,7 +358,7 @@ class QFieldCloudProjectsModel : public QAbstractListModel const QString &userRole, const ProjectCheckouts &checkout, const ProjectStatus &status, - const QDateTime &updatedAt, + const QDateTime &dataLastUpdatedAt, bool canRepackage, bool needsRepackaging ) : id( id ) @@ -365,7 +369,7 @@ class QFieldCloudProjectsModel : public QAbstractListModel , userRole( userRole ) , checkout( checkout ) , status( status ) - , updatedAt( updatedAt ) + , dataLastUpdatedAt( dataLastUpdatedAt ) , canRepackage( canRepackage ) , needsRepackaging( needsRepackaging ) {} @@ -382,10 +386,11 @@ class QFieldCloudProjectsModel : public QAbstractListModel ProjectErrorStatus errorStatus = ProjectErrorStatus::NoErrorStatus; ProjectCheckouts checkout; ProjectStatus status; - QDateTime updatedAt; + QDateTime dataLastUpdatedAt; bool canRepackage = false; bool needsRepackaging = false; bool isOutdated = false; + bool projectFileIsOutdated = false; ProjectModifications modification = ProjectModification::NoModification; QString localPath; @@ -416,8 +421,8 @@ class QFieldCloudProjectsModel : public QAbstractListModel QString lastLocalExportedAt; QString lastLocalExportId; QString lastLocalPushDeltas; - QDateTime lastLocalUpdatedAt; + QDateTime lastLocalDataLastUpdatedAt; QDateTime lastRefreshedAt; QMap jobs; }; diff --git a/src/qml/DashBoard.qml b/src/qml/DashBoard.qml index 98a4465239..3e01f2ac58 100644 --- a/src/qml/DashBoard.qml +++ b/src/qml/DashBoard.qml @@ -113,7 +113,7 @@ Drawer { return Theme.getThemeVectorIcon('ic_cloud_active_24dp'); } case QFieldCloudProjectsModel.Idle: - return cloudProjectsModel.currentProjectData.Outdated ? Theme.getThemeVectorIcon('ic_cloud_attention_24dp') : Theme.getThemeVectorIcon('ic_cloud_active_24dp'); + return cloudProjectsModel.currentProjectData.ProjectFileOutdated ? Theme.getThemeVectorIcon('ic_cloud_attention_24dp') : Theme.getThemeVectorIcon('ic_cloud_active_24dp'); default: Theme.getThemeVectorIcon( 'ic_cloud_24dp' ); } bgcolor: "transparent" diff --git a/src/qml/QFieldCloudPopup.qml b/src/qml/QFieldCloudPopup.qml index 72e5a65abe..5614a4fd87 100644 --- a/src/qml/QFieldCloudPopup.qml +++ b/src/qml/QFieldCloudPopup.qml @@ -611,7 +611,9 @@ Popup { if ( cloudConnection.status === QFieldCloudConnection.Connecting ) { displayToast(qsTr('Connecting cloud')) - } else if ( cloudProjectsModel.currentProjectData.Outdated ) { + } else if ( cloudProjectsModel.currentProjectData.ProjectFileOutdated ) { + displayToast(qsTr('This project has an updated project file on the cloud, you are advised to synchronize.'), 'warning') + } else if ( cloudProjectsModel.currentProjectData.ProjectOutdated ) { displayToast(qsTr('This project has updated data on the cloud, you should synchronize.')) } } diff --git a/src/qml/QFieldCloudScreen.qml b/src/qml/QFieldCloudScreen.qml index b9641fca68..112b647669 100644 --- a/src/qml/QFieldCloudScreen.qml +++ b/src/qml/QFieldCloudScreen.qml @@ -387,7 +387,7 @@ Page { break case QFieldCloudProjectsModel.LocalAndRemoteCheckout: status = qsTr( 'Available locally' ) - if (Outdated) { + if (ProjectOutdated) { status += qsTr( ', updated data available on the cloud'); } break diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index 4a3fecece8..5ffb6daee8 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -3326,6 +3326,10 @@ ApplicationWindow { if (cloudProjectsModel.layerObserver.deltaFileWrapper.hasError()) { cloudPopup.show() } + + if (cloudConnection.status === QFieldCloudConnection.LoggedIn) { + cloudProjectsModel.refreshProjectFileOutdatedStatus(cloudProjectId) + } } else { projectInfo.hasInsertRights = true projectInfo.hasEditRights = true @@ -3352,10 +3356,6 @@ ApplicationWindow { layoutListInstantiator.model.reloadModel() settings.setValue( "/QField/FirstRunFlag", false ) - - if (cloudProjectsModel.currentProjectData.Outdated) { - displayToast(qsTr('This project has updated data on the cloud, you should synchronize.')) - } } function onSetMapExtent(extent) { @@ -3593,6 +3593,11 @@ ApplicationWindow { displayToast(qsTr('Signed in')) // Go ahead and upload pending attachments in the background platformUtilities.uploadPendingAttachments(cloudConnection); + + var cloudProjectId = QFieldCloudUtils.getProjectId(qgisProject.fileName) + if (cloudProjectId !== '') { + cloudProjectsModel.refreshProjectFileOutdatedStatus(cloudProjectId) + } } previousStatus = cloudConnection.status } From e4623a82c6cb56cd4eb6379956f57300c3d4a230 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Thu, 11 Jan 2024 15:00:08 +0700 Subject: [PATCH 8/8] Apply review Co-authored-by: Ivan Ivanov --- src/qml/qgismobileapp.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index 5ffb6daee8..cfb2dee6ad 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -3595,7 +3595,7 @@ ApplicationWindow { platformUtilities.uploadPendingAttachments(cloudConnection); var cloudProjectId = QFieldCloudUtils.getProjectId(qgisProject.fileName) - if (cloudProjectId !== '') { + if (cloudProjectId) { cloudProjectsModel.refreshProjectFileOutdatedStatus(cloudProjectId) } }