diff --git a/app/activeproject.cpp b/app/activeproject.cpp index acfb53093..b0a875674 100644 --- a/app/activeproject.cpp +++ b/app/activeproject.cpp @@ -31,6 +31,7 @@ ActiveProject::ActiveProject( AppSettings &appSettings , ActiveLayer &activeLayer , LayersProxyModel &recordingLayerPM , LocalProjectsManager &localProjectsManager + , MerginApi *merginApi , QObject *parent ) : QObject( parent ) @@ -38,6 +39,7 @@ ActiveProject::ActiveProject( AppSettings &appSettings , mActiveLayer( activeLayer ) , mRecordingLayerPM( recordingLayerPM ) , mLocalProjectsManager( localProjectsManager ) + , mMerginApi( merginApi ) , mProjectLoadingLog( "" ) { // we used to have our own QgsProject instance, but unfortunately few pieces of qgis_core @@ -74,6 +76,17 @@ ActiveProject::ActiveProject( AppSettings &appSettings setAutosyncEnabled( mAppSettings.autosyncAllowed() ); QObject::connect( &mAppSettings, &AppSettings::autosyncAllowedChanged, this, &ActiveProject::setAutosyncEnabled ); + + QObject::connect( + mMerginApi, + &MerginApi::projectMetadataRoleUpdated, + this, [this]( const QString & projectFullName, const QString & role ) + { + if ( projectFullName == this->projectFullName() ) + { + setProjectRole( role ); + } + } ); } ActiveProject::~ActiveProject() = default; @@ -188,6 +201,7 @@ bool ActiveProject::forceLoad( const QString &filePath, bool force ) updateRecordingLayers(); updateActiveLayer(); updateMapSettingsLayers(); + updateUserRoleInActiveProject(); emit localProjectChanged( mLocalProject ); emit projectReloaded( mQgsProject ); @@ -553,3 +567,24 @@ bool ActiveProject::positionTrackingSupported() const return mQgsProject->readBoolEntry( QStringLiteral( "Mergin" ), QStringLiteral( "PositionTracking/Enabled" ), false ); } + +QString ActiveProject::projectRole() const +{ + return mProjectRole; +} + +void ActiveProject::setProjectRole( const QString &role ) +{ + if ( mProjectRole != role ) + { + mProjectRole = role; + + emit projectRoleChanged(); + } +} + +void ActiveProject::updateUserRoleInActiveProject() +{ + // update user's role each time a project is opened, following #3174 + mMerginApi->updateProjectMetadataRole( projectFullName() ); +} diff --git a/app/activeproject.h b/app/activeproject.h index b775303f6..faa166e60 100644 --- a/app/activeproject.h +++ b/app/activeproject.h @@ -22,6 +22,7 @@ #include "localprojectsmanager.h" #include "autosynccontroller.h" #include "inputmapsettings.h" +#include "merginapi.h" /** * \brief The ActiveProject class can load a QGIS project and holds its data. @@ -33,6 +34,7 @@ class ActiveProject: public QObject Q_PROPERTY( QgsProject *qgsProject READ qgsProject NOTIFY qgsProjectChanged ) // QgsProject instance of active project, never changes Q_PROPERTY( AutosyncController *autosyncController READ autosyncController NOTIFY autosyncControllerChanged ) Q_PROPERTY( InputMapSettings *mapSettings READ mapSettings WRITE setMapSettings NOTIFY mapSettingsChanged ) + Q_PROPERTY( QString projectRole READ projectRole WRITE setProjectRole NOTIFY projectRoleChanged ) Q_PROPERTY( QString mapTheme READ mapTheme WRITE setMapTheme NOTIFY mapThemeChanged ) Q_PROPERTY( bool positionTrackingSupported READ positionTrackingSupported NOTIFY positionTrackingSupportedChanged ) @@ -43,6 +45,7 @@ class ActiveProject: public QObject , ActiveLayer &activeLayer , LayersProxyModel &recordingLayerPM , LocalProjectsManager &localProjectsManager + , MerginApi *mMerginApi , QObject *parent = nullptr ); virtual ~ActiveProject(); @@ -118,6 +121,18 @@ class ActiveProject: public QObject bool positionTrackingSupported() const; + /** + * Returns role/permission level of current user for this project + */ + Q_INVOKABLE QString projectRole() const; + + void setProjectRole( const QString &role ); + + /** + * Calls Mergin API to update current project’s role + */ + void updateUserRoleInActiveProject(); + signals: void qgsProjectChanged(); void localProjectChanged( LocalProject project ); @@ -145,6 +160,8 @@ class ActiveProject: public QObject // Emited when the app (UI) should show tracking because there is a running tracking service void startPositionTracking(); + void projectRoleChanged(); + public slots: // Reloads project if current project path matches given path (its the same project) bool reloadProject( QString projectDir ); @@ -182,10 +199,12 @@ class ActiveProject: public QObject LayersProxyModel &mRecordingLayerPM; LocalProjectsManager &mLocalProjectsManager; InputMapSettings *mMapSettings = nullptr; + MerginApi *mMerginApi = nullptr; std::unique_ptr mAutosyncController; QString mProjectLoadingLog; + QString mProjectRole; /** * Reloads project. diff --git a/app/main.cpp b/app/main.cpp index 2ff044e83..7e87609b0 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -494,7 +494,7 @@ int main( int argc, char *argv[] ) LayersProxyModel recordingLpm( &lm, LayerModelTypes::ActiveLayerSelection ); ActiveLayer al; - ActiveProject activeProject( as, al, recordingLpm, localProjectsManager ); + ActiveProject activeProject( as, al, recordingLpm, localProjectsManager, ma.get() ); std::unique_ptr vm( new VariablesManager( ma.get() ) ); vm->registerInputExpressionFunctions(); diff --git a/app/qml/form/MMFormPage.qml b/app/qml/form/MMFormPage.qml index f12a9647f..1168a06d0 100644 --- a/app/qml/form/MMFormPage.qml +++ b/app/qml/form/MMFormPage.qml @@ -201,7 +201,7 @@ Page { footer: MMComponents.MMToolbar { - visible: !root.layerIsReadOnly + visible: !root.layerIsReadOnly && __activeProject.projectRole !== "reader" ObjectModel { id: readStateButtons @@ -231,7 +231,7 @@ Page { id: editGeometry text: qsTr( "Edit geometry" ) iconSource: __style.editIcon - visible: root.layerIsSpatial + visible: root.layerIsSpatial && __activeProject.projectRole !== "reader" onClicked: root.editGeometryRequested( root.controller.featureLayerPair ) } } diff --git a/app/qml/form/MMPreviewDrawer.qml b/app/qml/form/MMPreviewDrawer.qml index ca6753fc0..328d55ebb 100644 --- a/app/qml/form/MMPreviewDrawer.qml +++ b/app/qml/form/MMPreviewDrawer.qml @@ -295,7 +295,7 @@ Item { property bool isHTMLType: root.controller.type === MM.AttributePreviewController.HTML property bool isEmptyType: root.controller.type === MM.AttributePreviewController.Empty - property bool showEditButton: !root.layerIsReadOnly + property bool showEditButton: !root.layerIsReadOnly && __activeProject.projectRole !== "reader" property bool showStakeoutButton: __inputUtils.isPointLayerFeature( controller.featureLayerPair ) property bool showButtons: showEditButton || showStakeoutButton diff --git a/app/qml/form/components/MMFeaturesListPageDrawer.qml b/app/qml/form/components/MMFeaturesListPageDrawer.qml index c6f3a266a..ce9c22933 100644 --- a/app/qml/form/components/MMFeaturesListPageDrawer.qml +++ b/app/qml/form/components/MMFeaturesListPageDrawer.qml @@ -103,6 +103,7 @@ Drawer { } text: qsTr( "Add feature" ) + visible: __activeProject.projectRole !== "reader" onClicked: root.buttonClicked() } diff --git a/app/qml/layers/MMFeaturesListPage.qml b/app/qml/layers/MMFeaturesListPage.qml index 04b8bb256..5bf9c3f9f 100644 --- a/app/qml/layers/MMFeaturesListPage.qml +++ b/app/qml/layers/MMFeaturesListPage.qml @@ -89,7 +89,7 @@ MMComponents.MMPage { anchors.bottom: parent.bottom anchors.bottomMargin: root.hasToolbar ? __style.margin20 : ( __style.safeAreaBottom + __style.margin8 ) - visible: __inputUtils.isNoGeometryLayer( root.selectedLayer ) && !root.layerIsReadOnly + visible: __inputUtils.isNoGeometryLayer( root.selectedLayer ) && !root.layerIsReadOnly && __activeProject.projectRole !== "reader" text: qsTr("Add feature") diff --git a/app/qml/main.qml b/app/qml/main.qml index 4769ffa73..fda528faf 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -276,6 +276,7 @@ ApplicationWindow { MMToolbarButton { text: qsTr("Add") iconSource: __style.addIcon + visible: __activeProject.projectRole !== "reader" onClicked: { if ( __recordingLayersModel.rowCount() > 0 ) { stateManager.state = "map" diff --git a/app/qml/project/MMProjectController.qml b/app/qml/project/MMProjectController.qml index 04f52008d..7ba3698af 100644 --- a/app/qml/project/MMProjectController.qml +++ b/app/qml/project/MMProjectController.qml @@ -48,6 +48,8 @@ Item { function setupProjectOpen( projectPath ) { if ( projectPath === __activeProject.localProject.qgisProjectFilePath ) { + // update user's role in project ( in case user has changed ) + __activeProject.updateUserRoleInActiveProject() // just hide the panel - project already loaded hidePanel() } diff --git a/app/test/testactiveproject.cpp b/app/test/testactiveproject.cpp index 8b14abd24..4e6edf70c 100644 --- a/app/test/testactiveproject.cpp +++ b/app/test/testactiveproject.cpp @@ -39,7 +39,7 @@ void TestActiveProject::testProjectValidations() ActiveLayer al; LayersModel lm; LayersProxyModel lpm( &lm, LayerModelTypes::ActiveLayerSelection ); - ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager() ); + ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager(), mApi ); QSignalSpy spyReportIssues( &activeProject, &ActiveProject::reportIssue ); QSignalSpy spyErrorsFound( &activeProject, &ActiveProject::loadingErrorFound ); @@ -66,7 +66,7 @@ void TestActiveProject::testProjectLoadFailure() ActiveLayer al; LayersModel lm; LayersProxyModel lpm( &lm, LayerModelTypes::ActiveLayerSelection ); - ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager() ); + ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager(), mApi ); mApi->localProjectsManager().addLocalProject( projectdir, projectname ); @@ -88,7 +88,7 @@ void TestActiveProject::testPositionTrackingFlag() ActiveLayer al; LayersModel lm; LayersProxyModel lpm( &lm, LayerModelTypes::ActiveLayerSelection ); - ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager() ); + ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager(), mApi ); // project "planes" - tracking not enabled QString projectDir = TestUtils::testDataDir() + "/planes/"; diff --git a/app/test/testmerginapi.cpp b/app/test/testmerginapi.cpp index 815563bef..5ec9f37a0 100644 --- a/app/test/testmerginapi.cpp +++ b/app/test/testmerginapi.cpp @@ -2369,7 +2369,7 @@ void TestMerginApi::testAutosync() MapThemesModel mtm; AppSettings as; ActiveLayer al; LayersModel lm; LayersProxyModel lpm( &lm, LayerModelTypes::ActiveLayerSelection ); - ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager() ); + ActiveProject activeProject( as, al, lpm, mApi->localProjectsManager(), mApi ); mApi->localProjectsManager().addLocalProject( projectdir, projectname ); diff --git a/core/merginapi.cpp b/core/merginapi.cpp index b385c6e9f..ae3f42651 100644 --- a/core/merginapi.cpp +++ b/core/merginapi.cpp @@ -3456,6 +3456,45 @@ bool MerginApi::writeData( const QByteArray &data, const QString &path ) return true; } +bool MerginApi::updateCachedProjectRole( const QString &projectFullName, const QString &newRole ) +{ + LocalProject project = mLocalProjects.projectFromMerginName( projectFullName ); + if ( !project.isValid() ) + { + return false; + } + + QString metadataPath = project.projectDir + "/" + sMetadataFile; + + QFile file( metadataPath ); + if ( !file.open( QIODevice::ReadOnly ) ) + { + return false; + } + + QByteArray data = file.readAll(); + file.close(); + + QJsonDocument doc = QJsonDocument::fromJson( data ); + if ( !doc.isObject() ) + { + return false; + } + + QJsonObject obj = doc.object(); + obj["role"] = newRole; + doc.setObject( obj ); + + if ( !file.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) + { + return false; + } + + bool success = ( file.write( doc.toJson() ) != -1 ); + file.close(); + + return success; +} void MerginApi::createPathIfNotExists( const QString &filePath ) { @@ -3952,3 +3991,59 @@ DownloadQueueItem::DownloadQueueItem( const QString &fp, qint64 s, int v, qint64 tempFileName = CoreUtils::uuidWithoutBraces( QUuid::createUuid() ); } +void MerginApi::updateProjectMetadataRole( const QString &projectFullName ) +{ + if ( projectFullName.isEmpty() ) + { + return; + } + + QString projectDir = mLocalProjects.projectFromMerginName( projectFullName ).projectDir; + MerginProjectMetadata cachedProjectMetadata = MerginProjectMetadata::fromCachedJson( projectDir + "/" + sMetadataFile ); + QString cachedRole = cachedProjectMetadata.role; + + QNetworkReply *reply = getProjectInfo( projectFullName ); + if ( !reply ) + { + emit projectMetadataRoleUpdated( projectFullName, cachedRole ); + return; + } + + reply->request().setAttribute( static_cast( AttrProjectFullName ), projectFullName ); + reply->request().setAttribute( static_cast( AttrCachedRole ), cachedRole ); + connect( reply, &QNetworkReply::finished, this, &MerginApi::updateProjectMetadataRoleReplyFinished ); +} + +void MerginApi::updateProjectMetadataRoleReplyFinished() +{ + QNetworkReply *r = qobject_cast( sender() ); + Q_ASSERT( r ); + + QString projectFullName = r->request().attribute( static_cast( AttrProjectFullName ) ).toString(); + QString cachedRole = r->request().attribute( static_cast( AttrCachedRole ) ).toString(); + + if ( r->error() == QNetworkReply::NoError ) + { + QByteArray data = r->readAll(); + MerginProjectMetadata serverProject = MerginProjectMetadata::fromJson( data ); + QString role = serverProject.role; + + if ( role != cachedRole ) + { + if ( updateCachedProjectRole( projectFullName, role ) ) + { + emit projectMetadataRoleUpdated( projectFullName, role ); + } + else + { + CoreUtils::log( "metadata", QString( "Failed to update cached role for project %1" ).arg( projectFullName ) ); + } + } + } + else + { + emit projectMetadataRoleUpdated( projectFullName, cachedRole ); + } + + r->deleteLater(); +} diff --git a/core/merginapi.h b/core/merginapi.h index 8e7ef31d7..99c1f820c 100644 --- a/core/merginapi.h +++ b/core/merginapi.h @@ -574,6 +574,11 @@ class MerginApi: public QObject */ bool apiSupportsWorkspaces(); + /** + * Updates project metadata role by fetching latest information from server. + */ + Q_INVOKABLE void updateProjectMetadataRole( const QString &projectFullName ); + signals: void apiSupportsSubscriptionsChanged(); void supportsSelectiveSyncChanged(); @@ -652,6 +657,7 @@ class MerginApi: public QObject void apiSupportsWorkspacesChanged(); void serverWasUpgraded(); + void projectMetadataRoleUpdated( const QString &projectFullName, const QString &role ); private slots: void listProjectsReplyFinished( QString requestId ); @@ -791,6 +797,10 @@ class MerginApi: public QObject bool projectFileHasBeenUpdated( const ProjectDiff &diff ); + void updateProjectMetadataRoleReplyFinished(); + + bool updateCachedProjectRole( const QString &projectFullName, const QString &newRole ); + QNetworkAccessManager mManager; QString mApiRoot; LocalProjectsManager &mLocalProjects; @@ -807,6 +817,7 @@ class MerginApi: public QObject AttrTempFileName = QNetworkRequest::User + 1, AttrWorkspaceName = QNetworkRequest::User + 2, AttrAcceptFlag = QNetworkRequest::User + 3, + AttrCachedRole = QNetworkRequest::User + 4 }; Transactions mTransactionalStatus; //projectFullname -> transactionStatus diff --git a/core/merginprojectmetadata.cpp b/core/merginprojectmetadata.cpp index ba1ff95c3..7b4f2610e 100644 --- a/core/merginprojectmetadata.cpp +++ b/core/merginprojectmetadata.cpp @@ -99,6 +99,7 @@ MerginProjectMetadata MerginProjectMetadata::fromJson( const QByteArray &data ) project.name = docObj.value( QStringLiteral( "name" ) ).toString(); project.projectNamespace = docObj.value( QStringLiteral( "namespace" ) ).toString(); + project.role = docObj.value( QStringLiteral( "role" ) ).toString(); QString versionStr = docObj.value( QStringLiteral( "version" ) ).toString(); if ( versionStr.isEmpty() ) diff --git a/core/merginprojectmetadata.h b/core/merginprojectmetadata.h index b4bd178c8..90383ad23 100644 --- a/core/merginprojectmetadata.h +++ b/core/merginprojectmetadata.h @@ -59,6 +59,8 @@ struct MerginProjectMetadata { QString name; QString projectNamespace; + QString role; + QList writersnames; int version = -1; QList files; QString projectId; //!< unique project ID (only available in API that supports project IDs)