Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addition a project oudated and project >>file<< outdated states for cloud projects #4883

Merged
merged 8 commits into from
Jan 11, 2024
1 change: 1 addition & 0 deletions images/images.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
<file>themes/qfield/xxxhdpi/ic_close_black_24dp.png</file>
<file>themes/qfield/nodpi/ic_cloud_24dp.svg</file>
<file>themes/qfield/nodpi/ic_cloud_active_24dp.svg</file>
<file>themes/qfield/nodpi/ic_cloud_attention_24dp.svg</file>
<file>themes/qfield/nodpi/ic_cloud_project_48dp.svg</file>
<file>themes/qfield/hdpi/ic_cloud_project_48dp.png</file>
<file>themes/qfield/mdpi/ic_cloud_project_48dp.png</file>
Expand Down
2 changes: 2 additions & 0 deletions images/themes/qfield/nodpi/ic_cloud_attention_24dp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
88 changes: 82 additions & 6 deletions src/core/qfieldcloudprojectsmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>() << ProjectFileOutdatedRole );

if ( mCurrentProjectId == projectId )
{
emit currentProjectDataChanged();
}
}
}
}
} );
}

QString QFieldCloudProjectsModel::layerFileName( const QgsMapLayer *layer ) const
{
return layer->dataProvider()->dataSourceUri().split( '|' )[0];
Expand Down Expand Up @@ -482,18 +536,24 @@ 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->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 );
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 );

emit dataChanged( projectIndex, projectIndex, QVector<int>::fromList( roleNames().keys() ) );
emit projectRefreshed( projectId, refreshReason );

if ( mCurrentProjectId == projectId )
{
emit currentProjectDataChanged();
}
} );
}

Expand Down Expand Up @@ -1331,6 +1391,7 @@ void QFieldCloudProjectsModel::projectUpload( const QString &projectId, const bo

emit dataChanged( projectIndex, projectIndex, QVector<int>() << StatusRole );
emit pushFinished( projectId, false );
projectRefreshData( projectId, ProjectRefreshReason::DeltaUploaded );
}
} );

Expand Down Expand Up @@ -1395,6 +1456,7 @@ void QFieldCloudProjectsModel::projectUpload( const QString &projectId, const bo
{
emit dataChanged( projectIndex, projectIndex, QVector<int>() << StatusRole );
emit pushFinished( projectId, false );
projectRefreshData( projectId, ProjectRefreshReason::DeltaUploaded );
}
}
} );
Expand Down Expand Up @@ -1813,12 +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->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( "lastLocalDataLastUpdatedAt" ), project->lastLocalDataLastUpdatedAt );
QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "lastProjectFileSha256" ), QString() );
QFieldCloudUtils::setProjectSetting( projectId, QStringLiteral( "projectFileOudated" ), false );

rolesChanged << StatusRole << LocalPathRole << CheckoutRole << LastLocalExportedAtRole;
rolesChanged << StatusRole << ProjectOutdatedRole << ProjectFileOutdatedRole << LocalPathRole << CheckoutRole << LastLocalExportedAtRole;

emit dataChanged( projectIndex, projectIndex, rolesChanged );
emit projectDownloadFinished( projectId );
Expand All @@ -1842,6 +1911,8 @@ QHash<int, QByteArray> QFieldCloudProjectsModel::roleNames() const
roles[ModificationRole] = "Modification";
roles[CheckoutRole] = "Checkout";
roles[StatusRole] = "Status";
roles[ProjectOutdatedRole] = "ProjectOutdated";
roles[ProjectFileOutdatedRole] = "ProjectFileOutdated";
roles[ErrorStatusRole] = "ErrorStatus";
roles[ErrorStringRole] = "ErrorString";
roles[DownloadProgressRole] = "DownloadProgress";
Expand Down Expand Up @@ -1875,6 +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->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
Expand All @@ -1895,17 +1969,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 );
Expand Down Expand Up @@ -1956,7 +2028,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 ) );
Expand Down Expand Up @@ -2007,6 +2079,10 @@ QVariant QFieldCloudProjectsModel::data( const QModelIndex &index, int role ) co
return static_cast<int>( mProjects.at( index.row() )->checkout );
case StatusRole:
return static_cast<int>( mProjects.at( index.row() )->status );
case ProjectOutdatedRole:
return mProjects.at( index.row() )->isOutdated;
case ProjectFileOutdatedRole:
return mProjects.at( index.row() )->projectFileIsOutdated;
case ErrorStatusRole:
return static_cast<int>( mProjects.at( index.row() )->errorStatus );
case ErrorStringRole:
Expand Down
15 changes: 12 additions & 3 deletions src/core/qfieldcloudprojectsmodel.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class QFieldCloudProjectsModel : public QAbstractListModel
ModificationRole,
CheckoutRole,
StatusRole,
ProjectOutdatedRole,
ProjectFileOutdatedRole,
ErrorStatusRole,
ErrorStringRole,
DownloadProgressRole,
Expand Down Expand Up @@ -164,6 +166,7 @@ class QFieldCloudProjectsModel : public QAbstractListModel
enum class ProjectRefreshReason
{
Package,
DeltaUploaded
};

Q_ENUM( ProjectRefreshReason )
Expand Down Expand Up @@ -248,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<int, QByteArray> roleNames() const override;

Expand Down Expand Up @@ -350,9 +356,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 &dataLastUpdatedAt,
bool canRepackage,
bool needsRepackaging )
: id( id )
Expand All @@ -361,9 +367,9 @@ class QFieldCloudProjectsModel : public QAbstractListModel
, name( name )
, description( description )
, userRole( userRole )
, updatedAt( updatedAt )
, checkout( checkout )
, status( status )
, dataLastUpdatedAt( dataLastUpdatedAt )
, canRepackage( canRepackage )
, needsRepackaging( needsRepackaging )
{}
Expand All @@ -377,12 +383,14 @@ class QFieldCloudProjectsModel : public QAbstractListModel
QString name;
QString description;
QString userRole;
QString updatedAt;
ProjectErrorStatus errorStatus = ProjectErrorStatus::NoErrorStatus;
ProjectCheckouts checkout;
ProjectStatus status;
QDateTime dataLastUpdatedAt;
bool canRepackage = false;
bool needsRepackaging = false;
bool isOutdated = false;
bool projectFileIsOutdated = false;

ProjectModifications modification = ProjectModification::NoModification;
QString localPath;
Expand Down Expand Up @@ -414,6 +422,7 @@ class QFieldCloudProjectsModel : public QAbstractListModel
QString lastLocalExportId;
QString lastLocalPushDeltas;

QDateTime lastLocalDataLastUpdatedAt;
QDateTime lastRefreshedAt;
QMap<JobType, Job> jobs;
};
Expand Down
2 changes: 1 addition & 1 deletion src/qml/DashBoard.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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.ProjectFileOutdated ? Theme.getThemeVectorIcon('ic_cloud_attention_24dp') : Theme.getThemeVectorIcon('ic_cloud_active_24dp');
default: Theme.getThemeVectorIcon( 'ic_cloud_24dp' );
}
bgcolor: "transparent"
Expand Down
10 changes: 8 additions & 2 deletions src/qml/QFieldCloudPopup.qml
Original file line number Diff line number Diff line change
Expand Up @@ -605,11 +605,17 @@ 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.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.'))
}
}

function projectUpload(shouldDownloadUpdates) {
Expand Down
3 changes: 3 additions & 0 deletions src/qml/QFieldCloudScreen.qml
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,9 @@ Page {
break
case QFieldCloudProjectsModel.LocalAndRemoteCheckout:
status = qsTr( 'Available locally' )
if (ProjectOutdated) {
status += qsTr( ', updated data available on the cloud');
}
break
default:
break
Expand Down
9 changes: 9 additions & 0 deletions src/qml/qgismobileapp.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -3589,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
}
Expand Down
Loading