diff --git a/python/core/qgsprojectstorage.sip.in b/python/core/qgsprojectstorage.sip.in index 3cf2c3dd4daf..4fc8d0f237b7 100644 --- a/python/core/qgsprojectstorage.sip.in +++ b/python/core/qgsprojectstorage.sip.in @@ -24,6 +24,20 @@ and registered in QgsProjectStorageRegistry. #include "qgsprojectstorage.h" %End public: + + class Metadata +{ +%Docstring +Metadata associated with a project +%End + +%TypeHeaderCode +#include "qgsprojectstorage.h" +%End + public: + QDateTime lastModified; + }; + virtual ~QgsProjectStorage(); virtual QString type() = 0; @@ -63,6 +77,12 @@ was successful. %Docstring Rename an existing project at the given URI to a different URI. Returns true if renaming was successful. +%End + + virtual bool readProjectMetadata( const QString &uri, QgsProjectStorage::Metadata &metadata /Out/ ); +%Docstring +Reads project metadata (e.g. last modified time) if this is supported by the storage implementation. +Returns true if the metadata were read with success. %End virtual QString visibleName(); diff --git a/src/core/qgsprojectstorage.h b/src/core/qgsprojectstorage.h index 7ea80fca7b15..7009014e9f96 100644 --- a/src/core/qgsprojectstorage.h +++ b/src/core/qgsprojectstorage.h @@ -17,7 +17,9 @@ #define QGSPROJECTSTORAGE_H #include "qgis_core.h" +#include "qgis_sip.h" +#include #include class QIODevice; @@ -34,6 +36,14 @@ class QgsReadWriteContext; class CORE_EXPORT QgsProjectStorage { public: + + //! Metadata associated with a project + class Metadata + { + public: + QDateTime lastModified; + }; + virtual ~QgsProjectStorage(); /** @@ -73,6 +83,12 @@ class CORE_EXPORT QgsProjectStorage */ virtual bool renameProject( const QString &uri, const QString &uriNew ) { Q_UNUSED( uri ); Q_UNUSED( uriNew ); return false; } + /** + * Reads project metadata (e.g. last modified time) if this is supported by the storage implementation. + * Returns true if the metadata were read with success. + */ + virtual bool readProjectMetadata( const QString &uri, QgsProjectStorage::Metadata &metadata SIP_OUT ) { Q_UNUSED( uri ); Q_UNUSED( metadata ); return false; } + /** * Returns human-readable name of the storage. Used as the menu item text in QGIS. Empty name * indicates that the storage does not implement GUI support (showLoadGui() and showSaveGui()). diff --git a/src/providers/postgres/qgspostgresprojectstorage.cpp b/src/providers/postgres/qgspostgresprojectstorage.cpp index e78d2dc2a394..f0b52a2d1918 100644 --- a/src/providers/postgres/qgspostgresprojectstorage.cpp +++ b/src/providers/postgres/qgspostgresprojectstorage.cpp @@ -6,8 +6,32 @@ #include "qgsreadwritecontext.h" #include +#include +#include #include + +static bool _parseMetadataDocument( const QJsonDocument &doc, QgsProjectStorage::Metadata &metadata ) +{ + if ( !doc.isObject() ) + return false; + + QJsonObject docObj = doc.object(); + metadata.lastModified = QDateTime(); + if ( docObj.contains( "last_modified_time" ) ) + { + QString lastModifiedTimeStr = docObj["last_modified_time"].toString(); + if ( !lastModifiedTimeStr.isEmpty() ) + { + QDateTime lastModifiedUtc = QDateTime::fromString( lastModifiedTimeStr, Qt::ISODate ); + lastModifiedUtc.setTimeSpec( Qt::UTC ); + metadata.lastModified = lastModifiedUtc.toLocalTime(); + } + } + return true; +} + + static bool _projectsTableExists( QgsPostgresConn &conn, const QString &schemaName ) { QString tableName( "qgis_projects" ); @@ -123,13 +147,18 @@ bool QgsPostgresProjectStorage::writeProject( const QString &uri, QIODevice *dev // read from device and write to the table QByteArray content = device->readAll(); - QString metadata = "{ \"last_modified\": 123 }"; // TODO: real metadata + QString metadataExpr = QStringLiteral( "(%1 || (now() at time zone 'utc')::text || %2 || current_user || %3)::jsonb" ).arg( + QgsPostgresConn::quotedValue( "{ \"last_modified_time\": \"" ), + QgsPostgresConn::quotedValue( "\", \"last_modified_user\": \"" ), + QgsPostgresConn::quotedValue( "\" }" ) + ); // TODO: would be useful to have QByteArray version of PQexec() to avoid bytearray -> string -> bytearray conversion QString sql( "INSERT INTO %1.qgis_projects VALUES (%2, %3, E'\\\\x" ); sql = sql.arg( QgsPostgresConn::quotedIdentifier( projectUri.schemaName ), QgsPostgresConn::quotedValue( projectUri.projectName ), - QgsPostgresConn::quotedValue( metadata ) ); + metadataExpr // no need to quote: already quoted + ); sql += QString::fromAscii( content.toHex() ); sql += "') ON CONFLICT (name) DO UPDATE SET content = EXCLUDED.content, metadata = EXCLUDED.metadata;"; @@ -164,6 +193,33 @@ bool QgsPostgresProjectStorage::removeProject( const QString &uri ) } +bool QgsPostgresProjectStorage::readProjectMetadata( const QString &uri, QgsProjectStorage::Metadata &metadata ) +{ + QgsPostgresProjectUri projectUri = parseUri( uri ); + if ( !projectUri.valid ) + return false; + + QgsPostgresConn *conn = QgsPostgresConnPool::instance()->acquireConnection( projectUri.connInfo.connectionInfo( false ) ); + + bool ok = false; + QString sql( QStringLiteral( "SELECT metadata FROM %1.qgis_projects WHERE name = %2" ).arg( QgsPostgresConn::quotedIdentifier( projectUri.schemaName ), QgsPostgresConn::quotedValue( projectUri.projectName ) ) ); + QgsPostgresResult result( conn->PQexec( sql ) ); + if ( result.PQresultStatus() == PGRES_TUPLES_OK ) + { + if ( result.PQntuples() == 1 ) + { + QString metadataStr = result.PQgetvalue( 0, 0 ); + QJsonDocument doc( QJsonDocument::fromJson( metadataStr.toUtf8() ) ); + ok = _parseMetadataDocument( doc, metadata ); + } + } + + QgsPostgresConnPool::instance()->releaseConnection( conn ); + + return ok; +} + + QgsPostgresProjectUri QgsPostgresProjectStorage::parseUri( const QString &uri ) { QUrl u = QUrl::fromEncoded( uri.toUtf8() ); diff --git a/src/providers/postgres/qgspostgresprojectstorage.h b/src/providers/postgres/qgspostgresprojectstorage.h index 2b97bec3a9c6..94f0ed41aec5 100644 --- a/src/providers/postgres/qgspostgresprojectstorage.h +++ b/src/providers/postgres/qgspostgresprojectstorage.h @@ -35,6 +35,8 @@ class QgsPostgresProjectStorage : public QgsProjectStorage virtual bool removeProject( const QString &uri ) override; + virtual bool readProjectMetadata( const QString &uri, QgsProjectStorage::Metadata &metadata ) override; + private: static QgsPostgresProjectUri parseUri( const QString &uri ); }; diff --git a/tests/src/python/test_project_storage_postgres.py b/tests/src/python/test_project_storage_postgres.py index 1f48b4a5d0fb..fd57bcb143b8 100644 --- a/tests/src/python/test_project_storage_postgres.py +++ b/tests/src/python/test_project_storage_postgres.py @@ -28,6 +28,7 @@ QgsVectorLayer, QgsProject, ) +from PyQt5.QtCore import QDateTime from qgis.testing import start_app, unittest from utilities import unitTestDataPath @@ -101,6 +102,15 @@ def testSaveLoadProject(self): self.assertEqual(len(prj2.mapLayers()), 1) + # try to see project's metadata + + res, metadata = prj_storage.readProjectMetadata(project_uri) + self.assertTrue(res) + time_project = metadata.lastModified + time_now = QDateTime.currentDateTime() + time_diff = time_now.secsTo(time_project) + self.assertTrue(abs(time_diff) < 10) + # try to remove the project res = prj_storage.removeProject(project_uri)