From 107f3169bff2e4ca648283d2806c194ea826f521 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Tue, 20 May 2025 19:16:31 +0200 Subject: [PATCH 01/14] [ogr] fix JSON array interpreted as QVariantMap Fix #61728 --- src/core/providers/ogr/qgsogrprovider.cpp | 36 ++++- src/core/qgsogrutils.cpp | 1 - tests/src/core/testqgsogrprovider.cpp | 165 ++++++++++++++++++++++ 3 files changed, 199 insertions(+), 3 deletions(-) diff --git a/src/core/providers/ogr/qgsogrprovider.cpp b/src/core/providers/ogr/qgsogrprovider.cpp index 4e26aa8a67f..091bfcf93a2 100644 --- a/src/core/providers/ogr/qgsogrprovider.cpp +++ b/src/core/providers/ogr/qgsogrprovider.cpp @@ -886,9 +886,9 @@ void QgsOgrProvider::loadFields() QMutexLocker locker( datasetMutex ); #endif - for ( int i = 0; i < fdef.GetFieldCount(); ++i ) + for ( int fieldIndex = 0; fieldIndex < fdef.GetFieldCount(); ++fieldIndex ) { - OGRFieldDefnH fldDef = fdef.GetFieldDefn( i ); + OGRFieldDefnH fldDef = fdef.GetFieldDefn( fieldIndex ); const OGRFieldType ogrType = OGR_Fld_GetType( fldDef ); const OGRFieldSubType ogrSubType = OGR_Fld_GetSubType( fldDef ); @@ -896,6 +896,38 @@ void QgsOgrProvider::loadFields() QMetaType::Type varSubType = QMetaType::Type::UnknownType; QgsOgrUtils::ogrFieldTypeToQVariantType( ogrType, ogrSubType, varType, varSubType ); + // Handle special case for OGRFieldType::OFSTJSON which is not always a map. + // If subtype is JSON and varType is map, try to load a feature and check if it's + // really an object (rather than something else like an array) + // fallback to string given that only JSON object (map) is supported. + if ( varType == QMetaType::Type::QVariantMap && ogrSubType == OFSTJSON ) + { + QRecursiveMutex *layerMutex = nullptr; + OGRLayerH ogrLayer = mOgrLayer->getHandleAndMutex( layerMutex ); + QMutexLocker layerLocker( layerMutex ); + gdal::ogr_feature_unique_ptr f( OGR_L_GetNextFeature( ogrLayer ) ); + if ( f ) + { + const char *json = OGR_F_GetFieldAsString( f.get(), fieldIndex ); + if ( json && json[0] != '\0' ) + { + try + { + const auto json_element = json::parse( json ); + if ( ! json_element.is_object() ) + { + varType = QMetaType::Type::QString; + } + } + catch ( const json::parse_error & ) + { + varType = QMetaType::Type::QString; + } + } + OGR_L_ResetReading( ogrLayer ); + } + } + //TODO: fix this hack #ifdef ANDROID QString name = OGR_Fld_GetNameRef( fldDef ); diff --git a/src/core/qgsogrutils.cpp b/src/core/qgsogrutils.cpp index 7a7f05c05ae..3d7bec7bbee 100644 --- a/src/core/qgsogrutils.cpp +++ b/src/core/qgsogrutils.cpp @@ -1862,7 +1862,6 @@ void QgsOgrUtils::ogrFieldTypeToQVariantType( OGRFieldType ogrType, OGRFieldSubT case OFTWideString: if ( ogrSubType == OFSTJSON ) { - ogrSubType = OFSTJSON; variantType = QMetaType::Type::QVariantMap; variantSubType = QMetaType::Type::QString; } diff --git a/tests/src/core/testqgsogrprovider.cpp b/tests/src/core/testqgsogrprovider.cpp index 56e5fc427e3..34fdace65cd 100644 --- a/tests/src/core/testqgsogrprovider.cpp +++ b/tests/src/core/testqgsogrprovider.cpp @@ -58,6 +58,8 @@ class TestQgsOgrProvider : public QgsTest void testExtent(); void testVsiCredentialOptions(); void testVsiCredentialOptionsQuerySublayers(); + void testJSONFields_data(); + void testJSONFields(); private: QString mTestDataDir; @@ -579,5 +581,168 @@ void TestQgsOgrProvider::testVsiCredentialOptionsQuerySublayers() } +void TestQgsOgrProvider::testJSONFields_data() +{ + QTest::addColumn( "jsonData" ); + QTest::addColumn( "expectedType" ); + + QTest::newRow( "array of map" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "array_of_map": [ + { + "a": 1, + "b": 2.0 + } + ] + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QString ); + + QTest::newRow( "map" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "map": { + "a": 1, + "b": 2.0 + } + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QVariantMap ); + + QTest::newRow( "int" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "int": 1 + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::Int ); + + QTest::newRow( "stringlist" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "string_list": [ "a", "b", "c" ] + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QStringList ); + + QTest::newRow( "string" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "string": "a" + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QString ); + + QTest::newRow( "double" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "double": 1.0 + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::Double ); + + QTest::newRow( "bool" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "bool": true + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::Bool ); + + QTest::newRow( "int list" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "int_list": [1, 2, 3] + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QVariantList ); + + QTest::newRow( "real list" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "real_list": [1.0, 2.1, 3] + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QVariantList ); +} + +void TestQgsOgrProvider::testJSONFields() +{ + QFETCH( QString, jsonData ); + QFETCH( int, expectedType ); + + QTemporaryDir dir; + QString filePath = dir.path() + "/test.json"; + QFile file( filePath ); + if ( file.open( QIODevice::WriteOnly ) ) + { + QTextStream textStream( &file ); + textStream << jsonData; + file.close(); + } + QgsVectorLayer layer { filePath, QStringLiteral( "json" ), QLatin1String( "ogr" ) }; + QVERIFY( layer.isValid() ); + QgsFields fields = layer.fields(); + QCOMPARE( fields.count(), 1 ); + QgsField field = fields.at( 0 ); + QCOMPARE( static_cast( field.type() ), expectedType ); +} + + QGSTEST_MAIN( TestQgsOgrProvider ) #include "testqgsogrprovider.moc" From 0dd8898075e40df2395b562a9faf4dfb4b2d5bea Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Wed, 21 May 2025 12:13:45 +0200 Subject: [PATCH 02/14] Removed wrong assumption about JSON -> QVariantMap ... long awaited --- src/core/providers/ogr/qgsogrprovider.cpp | 54 ++++++++- src/core/qgsogrutils.cpp | 126 +++++++++++++-------- tests/src/core/testqgsogrprovider.cpp | 17 ++- tests/src/core/testqgsvectorfilewriter.cpp | 6 +- 4 files changed, 147 insertions(+), 56 deletions(-) diff --git a/src/core/providers/ogr/qgsogrprovider.cpp b/src/core/providers/ogr/qgsogrprovider.cpp index 091bfcf93a2..04208f032f6 100644 --- a/src/core/providers/ogr/qgsogrprovider.cpp +++ b/src/core/providers/ogr/qgsogrprovider.cpp @@ -896,11 +896,11 @@ void QgsOgrProvider::loadFields() QMetaType::Type varSubType = QMetaType::Type::UnknownType; QgsOgrUtils::ogrFieldTypeToQVariantType( ogrType, ogrSubType, varType, varSubType ); - // Handle special case for OGRFieldType::OFSTJSON which is not always a map. - // If subtype is JSON and varType is map, try to load a feature and check if it's + // Handle special case for OGRFieldType::OFSTJSON which is not necessarily a map. + // If subtype is JSON try to load a feature and check if it's // really an object (rather than something else like an array) - // fallback to string given that only JSON object (map) is supported. - if ( varType == QMetaType::Type::QVariantMap && ogrSubType == OFSTJSON ) + // fallback to string. + if ( ogrType == OFTString && ogrSubType == OFSTJSON ) { QRecursiveMutex *layerMutex = nullptr; OGRLayerH ogrLayer = mOgrLayer->getHandleAndMutex( layerMutex ); @@ -913,14 +913,56 @@ void QgsOgrProvider::loadFields() { try { - const auto json_element = json::parse( json ); - if ( ! json_element.is_object() ) + const nlohmann::json json_element = json::parse( json ); + if ( json_element.is_array() ) { + bool allStrings = true; + bool allInts = true; + bool allDoubles = true; + for ( const auto &item : json_element ) + { + if ( !item.is_string() ) + { + allStrings = false; + } + if ( !item.is_number_integer() ) + { + allInts = false; + } + if ( !item.is_number_float() ) + { + allDoubles = false; + } + } + if ( allStrings ) + { + varType = QMetaType::Type::QStringList; + } + else if ( allInts ) + { + varType = QMetaType::Type::QVariantList; + varSubType = QMetaType::Type::LongLong; + } + else if ( allDoubles ) + { + varType = QMetaType::Type::QVariantList; + varSubType = QMetaType::Type::Double; + } + else + { + QgsDebugMsgLevel( QStringLiteral( "JSON array contains mixed types, defaulting to string" ), 2 ); + varType = QMetaType::Type::QString; + } + } + else if ( ! json_element.is_object() ) + { + QgsDebugMsgLevel( QStringLiteral( "JSON is neither an array nor an object, defaulting to string" ), 2 ); varType = QMetaType::Type::QString; } } catch ( const json::parse_error & ) { + QgsDebugMsgLevel( QStringLiteral( "JSON is not valid, defaulting to string" ), 2 ); varType = QMetaType::Type::QString; } } diff --git a/src/core/qgsogrutils.cpp b/src/core/qgsogrutils.cpp index 3d7bec7bbee..db0feae5481 100644 --- a/src/core/qgsogrutils.cpp +++ b/src/core/qgsogrutils.cpp @@ -38,6 +38,7 @@ #include "qgsfontmanager.h" #include "qgsvariantutils.h" #include "qgsogrproviderutils.h" +#include "qgsjsonutils.h" #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,6,0) #include "qgsweakrelation.h" @@ -59,6 +60,7 @@ #include "ogr_srs_api.h" +#include "nlohmann/json.hpp" void gdal::OGRDataSourceDeleter::operator()( OGRDataSourceH source ) const { @@ -541,6 +543,23 @@ QVariant QgsOgrUtils::getOgrFeatureAttribute( OGRFeatureH ogrFet, const QgsField if ( ok ) *ok = true; + + auto getJsonValue = [&]() -> bool + { + const char *json = OGR_F_GetFieldAsString( ogrFet, attIndex ); + try + { + const nlohmann::json json_element = json::parse( json ); + value = QgsJsonUtils::jsonToVariant( json_element ); + return true; + } + catch ( const json::parse_error &e ) + { + QgsDebugMsgLevel( QStringLiteral( "Error parsing JSON: %1" ).arg( e.what() ), 2 ); + return false; + } + }; + if ( OGR_F_IsFieldSetAndNotNull( ogrFet, attIndex ) ) { switch ( field.type() ) @@ -614,21 +633,24 @@ QVariant QgsOgrUtils::getOgrFeatureAttribute( OGRFeatureH ogrFet, const QgsField case QMetaType::Type::QStringList: { - QStringList list; - char **lst = OGR_F_GetFieldAsStringList( ogrFet, attIndex ); - const int count = CSLCount( lst ); - if ( count > 0 ) + if ( field.typeName() != QStringLiteral( "JSON" ) || ! getJsonValue() ) { - list.reserve( count ); - for ( int i = 0; i < count; i++ ) + QStringList list; + char **lst = OGR_F_GetFieldAsStringList( ogrFet, attIndex ); + const int count = CSLCount( lst ); + if ( count > 0 ) { - if ( encoding ) - list << encoding->toUnicode( lst[i] ); - else - list << QString::fromUtf8( lst[i] ); + list.reserve( count ); + for ( int i = 0; i < count; i++ ) + { + if ( encoding ) + list << encoding->toUnicode( lst[i] ); + else + list << QString::fromUtf8( lst[i] ); + } } + value = list; } - value = list; break; } @@ -638,72 +660,84 @@ QVariant QgsOgrUtils::getOgrFeatureAttribute( OGRFeatureH ogrFet, const QgsField { case QMetaType::Type::QString: { - QStringList list; - char **lst = OGR_F_GetFieldAsStringList( ogrFet, attIndex ); - const int count = CSLCount( lst ); - if ( count > 0 ) + if ( field.typeName() != QStringLiteral( "JSON" ) || ! getJsonValue() ) { - list.reserve( count ); - for ( int i = 0; i < count; i++ ) + QStringList list; + char **lst = OGR_F_GetFieldAsStringList( ogrFet, attIndex ); + const int count = CSLCount( lst ); + if ( count > 0 ) { - if ( encoding ) - list << encoding->toUnicode( lst[i] ); - else - list << QString::fromUtf8( lst[i] ); + list.reserve( count ); + for ( int i = 0; i < count; i++ ) + { + if ( encoding ) + list << encoding->toUnicode( lst[i] ); + else + list << QString::fromUtf8( lst[i] ); + } } + value = list; } - value = list; break; } case QMetaType::Type::Int: { - QVariantList list; - int count = 0; - const int *lst = OGR_F_GetFieldAsIntegerList( ogrFet, attIndex, &count ); - if ( count > 0 ) + if ( field.typeName() != QStringLiteral( "JSON" ) || ! getJsonValue() ) { - list.reserve( count ); - for ( int i = 0; i < count; i++ ) + QVariantList list; + int count = 0; + const int *lst = OGR_F_GetFieldAsIntegerList( ogrFet, attIndex, &count ); + if ( count > 0 ) { - list << lst[i]; + list.reserve( count ); + for ( int i = 0; i < count; i++ ) + { + list << lst[i]; + } } + value = list; } - value = list; break; } case QMetaType::Type::Double: { - QVariantList list; - int count = 0; - const double *lst = OGR_F_GetFieldAsDoubleList( ogrFet, attIndex, &count ); - if ( count > 0 ) + if ( field.typeName() != QStringLiteral( "JSON" ) || ! getJsonValue() ) { - list.reserve( count ); - for ( int i = 0; i < count; i++ ) + QVariantList list; + int count = 0; + const double *lst = OGR_F_GetFieldAsDoubleList( ogrFet, attIndex, &count ); + if ( count > 0 ) { - list << lst[i]; + list.reserve( count ); + for ( int i = 0; i < count; i++ ) + { + list << lst[i]; + } } + value = list; } - value = list; break; } case QMetaType::Type::LongLong: { - QVariantList list; - int count = 0; - const long long *lst = OGR_F_GetFieldAsInteger64List( ogrFet, attIndex, &count ); - if ( count > 0 ) + if ( field.typeName() != QStringLiteral( "JSON" ) || ! getJsonValue() ) { - list.reserve( count ); - for ( int i = 0; i < count; i++ ) + QVariantList list; + int count = 0; + const long long *lst = OGR_F_GetFieldAsInteger64List( ogrFet, attIndex, &count ); + if ( count > 0 ) { - list << lst[i]; + list.reserve( count ); + for ( int i = 0; i < count; i++ ) + { + list << lst[i]; + } } + value = list; } - value = list; break; } diff --git a/tests/src/core/testqgsogrprovider.cpp b/tests/src/core/testqgsogrprovider.cpp index 34fdace65cd..2e47593156a 100644 --- a/tests/src/core/testqgsogrprovider.cpp +++ b/tests/src/core/testqgsogrprovider.cpp @@ -586,7 +586,7 @@ void TestQgsOgrProvider::testJSONFields_data() QTest::addColumn( "jsonData" ); QTest::addColumn( "expectedType" ); - QTest::newRow( "array of map" ) << QStringLiteral( R"json( + QTest::newRow( "array of map string fallback" ) << QStringLiteral( R"json( { "type": "FeatureCollection", "features": [ @@ -719,6 +719,21 @@ void TestQgsOgrProvider::testJSONFields_data() ] } )json" ) << static_cast( QMetaType::Type::QVariantList ); + + + QTest::newRow( "array mixed types string fallback" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "mixed_list": [1, 2.0, "a", true] + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QString ); } void TestQgsOgrProvider::testJSONFields() diff --git a/tests/src/core/testqgsvectorfilewriter.cpp b/tests/src/core/testqgsvectorfilewriter.cpp index eefcead37b1..e6b65ee5379 100644 --- a/tests/src/core/testqgsvectorfilewriter.cpp +++ b/tests/src/core/testqgsvectorfilewriter.cpp @@ -568,10 +568,10 @@ void TestQgsVectorFileWriter::testExportArrayToGpkg() const QgsVectorLayer vl2( QStringLiteral( "%1|layername=test" ).arg( fileName ), "src_test", "ogr" ); QVERIFY( vl2.isValid() ); QCOMPARE( vl2.featureCount(), 1L ); - QCOMPARE( vl2.fields().at( 1 ).type(), QMetaType::Type::QVariantMap ); - QCOMPARE( vl2.fields().at( 1 ).subType(), QMetaType::Type::QString ); + QCOMPARE( vl2.fields().at( 1 ).type(), QMetaType::Type::QVariantList ); + QCOMPARE( vl2.fields().at( 1 ).subType(), QMetaType::Type::LongLong ); QCOMPARE( vl2.fields().at( 1 ).typeName(), QStringLiteral( "JSON" ) ); - QCOMPARE( vl2.fields().at( 2 ).type(), QMetaType::Type::QVariantMap ); + QCOMPARE( vl2.fields().at( 2 ).type(), QMetaType::Type::QStringList ); QCOMPARE( vl2.fields().at( 2 ).subType(), QMetaType::Type::QString ); QCOMPARE( vl2.fields().at( 2 ).typeName(), QStringLiteral( "JSON" ) ); QCOMPARE( vl2.getFeature( 1 ).attribute( 1 ).toList(), QVariantList() << 1 << 2 << 3 ); From 2a3475ac964d94a585ce8e63c5566f489b19377f Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Wed, 21 May 2025 14:44:43 +0200 Subject: [PATCH 03/14] Rely on OGR for most array identification --- src/core/providers/ogr/qgsogrprovider.cpp | 81 +++++++++++++++------- src/core/qgsogrutils.cpp | 29 +++++--- tests/src/core/testqgsogrprovider.cpp | 68 +++++++++++++++--- tests/src/core/testqgsvectorfilewriter.cpp | 2 +- 4 files changed, 133 insertions(+), 47 deletions(-) diff --git a/src/core/providers/ogr/qgsogrprovider.cpp b/src/core/providers/ogr/qgsogrprovider.cpp index 04208f032f6..349b240fb10 100644 --- a/src/core/providers/ogr/qgsogrprovider.cpp +++ b/src/core/providers/ogr/qgsogrprovider.cpp @@ -144,7 +144,7 @@ bool QgsOgrProvider::convertField( QgsField &field, const QTextCodec &encoding ) } else { - // other lists are supported at this moment + // other lists are not supported at this moment return false; } break; @@ -900,7 +900,9 @@ void QgsOgrProvider::loadFields() // If subtype is JSON try to load a feature and check if it's // really an object (rather than something else like an array) // fallback to string. - if ( ogrType == OFTString && ogrSubType == OFSTJSON ) + // Note that homogeneous arrays of numbers and strings are already correctly + // identified by OGR so we don't need to check them here. + if ( ( ogrType == OFTString || ogrType == OFTWideString ) && ogrSubType == OFSTJSON ) { QRecursiveMutex *layerMutex = nullptr; OGRLayerH ogrLayer = mOgrLayer->getHandleAndMutex( layerMutex ); @@ -914,56 +916,87 @@ void QgsOgrProvider::loadFields() try { const nlohmann::json json_element = json::parse( json ); + // Check if it's an homogeneous array of numbers or strings if ( json_element.is_array() ) { + // Check whether the values are all of the same type + bool allNumbers = true; + bool allIntegers = true; bool allStrings = true; - bool allInts = true; - bool allDoubles = true; - for ( const auto &item : json_element ) + for ( auto &value : json_element ) { - if ( !item.is_string() ) + if ( allStrings && !value.is_string() ) { allStrings = false; } - if ( !item.is_number_integer() ) + if ( allNumbers && !value.is_number() ) { - allInts = false; + allNumbers = false; } - if ( !item.is_number_float() ) + if ( allIntegers && !value.is_number_integer() ) { - allDoubles = false; + allIntegers = false; } } - if ( allStrings ) + if ( allNumbers ) + { + if ( allIntegers ) + { + varType = QMetaType::Type::QVariantList; + varSubType = QMetaType::Type::LongLong; + } + else + { + varType = QMetaType::Type::QVariantList; + varSubType = QMetaType::Type::Double; + } + } + else if ( allStrings ) { varType = QMetaType::Type::QStringList; - } - else if ( allInts ) - { - varType = QMetaType::Type::QVariantList; - varSubType = QMetaType::Type::LongLong; - } - else if ( allDoubles ) - { - varType = QMetaType::Type::QVariantList; - varSubType = QMetaType::Type::Double; + varSubType = QMetaType::Type::UnknownType; } else { - QgsDebugMsgLevel( QStringLiteral( "JSON array contains mixed types, defaulting to string" ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "JSON array contains mixed types, falling back to string" ), 2 ); varType = QMetaType::Type::QString; + varSubType = QMetaType::Type::UnknownType; } } else if ( ! json_element.is_object() ) { - QgsDebugMsgLevel( QStringLiteral( "JSON is neither an array nor an object, defaulting to string" ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "JSON is neither an array nor an object, falling back to string" ), 2 ); varType = QMetaType::Type::QString; + varSubType = QMetaType::Type::UnknownType; + } + else if ( json_element.is_number() ) + { + if ( json_element.is_number_float() ) + { + varType = QMetaType::Type::Double; + varSubType = QMetaType::Type::UnknownType; + } + else + { + varType = QMetaType::Type::LongLong; + varSubType = QMetaType::Type::UnknownType; + } + } + else if ( json_element.is_string() ) + { + varType = QMetaType::Type::QString; + varSubType = QMetaType::Type::UnknownType; + } + else + { + QgsDebugMsgLevel( QStringLiteral( "JSON is not valid, falling back to string" ), 2 ); } } catch ( const json::parse_error & ) { - QgsDebugMsgLevel( QStringLiteral( "JSON is not valid, defaulting to string" ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "JSON is not valid, falling back to string" ), 2 ); varType = QMetaType::Type::QString; + varSubType = QMetaType::Type::UnknownType; } } OGR_L_ResetReading( ogrLayer ); diff --git a/src/core/qgsogrutils.cpp b/src/core/qgsogrutils.cpp index db0feae5481..d3ddffbd620 100644 --- a/src/core/qgsogrutils.cpp +++ b/src/core/qgsogrutils.cpp @@ -547,9 +547,15 @@ QVariant QgsOgrUtils::getOgrFeatureAttribute( OGRFeatureH ogrFet, const QgsField auto getJsonValue = [&]() -> bool { const char *json = OGR_F_GetFieldAsString( ogrFet, attIndex ); + QString jsonContent; + if ( encoding ) + jsonContent = encoding->toUnicode( json ).toUtf8(); + else + jsonContent = QString::fromUtf8( json ).toUtf8(); + try { - const nlohmann::json json_element = json::parse( json ); + const nlohmann::json json_element = json::parse( jsonContent.toStdString() ); value = QgsJsonUtils::jsonToVariant( json_element ); return true; } @@ -566,18 +572,20 @@ QVariant QgsOgrUtils::getOgrFeatureAttribute( OGRFeatureH ogrFet, const QgsField { case QMetaType::Type::QString: { - if ( encoding ) - value = QVariant( encoding->toUnicode( OGR_F_GetFieldAsString( ogrFet, attIndex ) ) ); - else - value = QVariant( QString::fromUtf8( OGR_F_GetFieldAsString( ogrFet, attIndex ) ) ); + if ( field.typeName() != QStringLiteral( "JSON" ) || ! getJsonValue() ) + { + if ( encoding ) + value = QVariant( encoding->toUnicode( OGR_F_GetFieldAsString( ogrFet, attIndex ) ) ); + else + value = QVariant( QString::fromUtf8( OGR_F_GetFieldAsString( ogrFet, attIndex ) ) ); #ifdef Q_OS_WIN - // Fixes GH #41076 (empty strings shown as NULL), because we have checked before that it was NOT NULL - // Note: QVariant( QString( ) ).isNull( ) is still true on windows so we really need string literal :( - if ( value.isNull() ) - value = QVariant( QStringLiteral( "" ) ); // skip-keyword-check + // Fixes GH #41076 (empty strings shown as NULL), because we have checked before that it was NOT NULL + // Note: QVariant( QString( ) ).isNull( ) is still true on windows so we really need string literal :( + if ( value.isNull() ) + value = QVariant( QStringLiteral( "" ) ); // skip-keyword-check #endif - + } break; } case QMetaType::Type::Int: @@ -1867,7 +1875,6 @@ void QgsOgrUtils::ogrFieldTypeToQVariantType( OGRFieldType ogrType, OGRFieldSubT if ( ogrSubType == OFSTBoolean ) { variantType = QMetaType::Type::Bool; - ogrSubType = OFSTBoolean; } else variantType = QMetaType::Type::Int; diff --git a/tests/src/core/testqgsogrprovider.cpp b/tests/src/core/testqgsogrprovider.cpp index 2e47593156a..58c4de6398b 100644 --- a/tests/src/core/testqgsogrprovider.cpp +++ b/tests/src/core/testqgsogrprovider.cpp @@ -585,6 +585,7 @@ void TestQgsOgrProvider::testJSONFields_data() { QTest::addColumn( "jsonData" ); QTest::addColumn( "expectedType" ); + QTest::addColumn( "expectedSubType" ); QTest::newRow( "array of map string fallback" ) << QStringLiteral( R"json( { @@ -603,9 +604,10 @@ void TestQgsOgrProvider::testJSONFields_data() } ] } -)json" ) << static_cast( QMetaType::Type::QString ); +)json" ) << static_cast( QMetaType::Type::QString ) + << static_cast( QMetaType::Type::UnknownType ); - QTest::newRow( "map" ) << QStringLiteral( R"json( + QTest::newRow( "simple map" ) << QStringLiteral( R"json( { "type": "FeatureCollection", "features": [ @@ -620,7 +622,26 @@ void TestQgsOgrProvider::testJSONFields_data() } ] } -)json" ) << static_cast( QMetaType::Type::QVariantMap ); +)json" ) << static_cast( QMetaType::Type::QVariantMap ) + << static_cast( QMetaType::Type::QString ); + + QTest::newRow( "complex map" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "map": { + "a": 1, + "b": [2.0, "c"] + } + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QVariantMap ) + << static_cast( QMetaType::Type::QString ); QTest::newRow( "int" ) << QStringLiteral( R"json( { @@ -634,7 +655,8 @@ void TestQgsOgrProvider::testJSONFields_data() } ] } -)json" ) << static_cast( QMetaType::Type::Int ); +)json" ) << static_cast( QMetaType::Type::Int ) + << static_cast( QMetaType::Type::UnknownType ); QTest::newRow( "stringlist" ) << QStringLiteral( R"json( { @@ -648,7 +670,8 @@ void TestQgsOgrProvider::testJSONFields_data() } ] } -)json" ) << static_cast( QMetaType::Type::QStringList ); +)json" ) << static_cast( QMetaType::Type::QStringList ) + << static_cast( QMetaType::Type::QString ); QTest::newRow( "string" ) << QStringLiteral( R"json( { @@ -662,7 +685,8 @@ void TestQgsOgrProvider::testJSONFields_data() } ] } -)json" ) << static_cast( QMetaType::Type::QString ); +)json" ) << static_cast( QMetaType::Type::QString ) + << static_cast( QMetaType::Type::UnknownType ); QTest::newRow( "double" ) << QStringLiteral( R"json( { @@ -676,7 +700,8 @@ void TestQgsOgrProvider::testJSONFields_data() } ] } -)json" ) << static_cast( QMetaType::Type::Double ); +)json" ) << static_cast( QMetaType::Type::Double ) + << static_cast( QMetaType::Type::UnknownType ); QTest::newRow( "bool" ) << QStringLiteral( R"json( { @@ -690,7 +715,8 @@ void TestQgsOgrProvider::testJSONFields_data() } ] } -)json" ) << static_cast( QMetaType::Type::Bool ); +)json" ) << static_cast( QMetaType::Type::Bool ) + << static_cast( QMetaType::Type::UnknownType ); QTest::newRow( "int list" ) << QStringLiteral( R"json( { @@ -704,7 +730,8 @@ void TestQgsOgrProvider::testJSONFields_data() } ] } -)json" ) << static_cast( QMetaType::Type::QVariantList ); +)json" ) << static_cast( QMetaType::Type::QVariantList ) + << static_cast( QMetaType::Type::Int ); QTest::newRow( "real list" ) << QStringLiteral( R"json( { @@ -718,7 +745,8 @@ void TestQgsOgrProvider::testJSONFields_data() } ] } -)json" ) << static_cast( QMetaType::Type::QVariantList ); +)json" ) << static_cast( QMetaType::Type::QVariantList ) + << static_cast( QMetaType::Type::Double ); QTest::newRow( "array mixed types string fallback" ) << QStringLiteral( R"json( @@ -733,13 +761,30 @@ void TestQgsOgrProvider::testJSONFields_data() } ] } -)json" ) << static_cast( QMetaType::Type::QString ); +)json" ) << static_cast( QMetaType::Type::QString ) + << static_cast( QMetaType::Type::UnknownType ); + + QTest::newRow( "array mixed numeric types" ) << QStringLiteral( R"json( +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "mixed_numeric_list": [1, 2.3] + } + } + ] +} +)json" ) << static_cast( QMetaType::Type::QVariantList ) + << static_cast( QMetaType::Type::Double ); } void TestQgsOgrProvider::testJSONFields() { QFETCH( QString, jsonData ); QFETCH( int, expectedType ); + QFETCH( int, expectedSubType ); QTemporaryDir dir; QString filePath = dir.path() + "/test.json"; @@ -756,6 +801,7 @@ void TestQgsOgrProvider::testJSONFields() QCOMPARE( fields.count(), 1 ); QgsField field = fields.at( 0 ); QCOMPARE( static_cast( field.type() ), expectedType ); + QCOMPARE( static_cast( field.subType() ), expectedSubType ); } diff --git a/tests/src/core/testqgsvectorfilewriter.cpp b/tests/src/core/testqgsvectorfilewriter.cpp index e6b65ee5379..ec16a742c0a 100644 --- a/tests/src/core/testqgsvectorfilewriter.cpp +++ b/tests/src/core/testqgsvectorfilewriter.cpp @@ -572,7 +572,7 @@ void TestQgsVectorFileWriter::testExportArrayToGpkg() QCOMPARE( vl2.fields().at( 1 ).subType(), QMetaType::Type::LongLong ); QCOMPARE( vl2.fields().at( 1 ).typeName(), QStringLiteral( "JSON" ) ); QCOMPARE( vl2.fields().at( 2 ).type(), QMetaType::Type::QStringList ); - QCOMPARE( vl2.fields().at( 2 ).subType(), QMetaType::Type::QString ); + QCOMPARE( vl2.fields().at( 2 ).subType(), QMetaType::Type::UnknownType ); QCOMPARE( vl2.fields().at( 2 ).typeName(), QStringLiteral( "JSON" ) ); QCOMPARE( vl2.getFeature( 1 ).attribute( 1 ).toList(), QVariantList() << 1 << 2 << 3 ); QCOMPARE( vl2.getFeature( 1 ).attribute( 2 ).toStringList(), QStringList() << "a" << "b" << "c" ); From 6a982c0bbbbc6bf666847df6be798808a38873af Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Thu, 22 May 2025 10:56:21 +0200 Subject: [PATCH 04/14] [wmts] Be nice and accept redundant spaces in coord tuples Fix #61399 --- src/providers/wms/qgswmscapabilities.cpp | 10 ++--- .../src/providers/testqgswmscapabilities.cpp | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/providers/wms/qgswmscapabilities.cpp b/src/providers/wms/qgswmscapabilities.cpp index 3b09bfc9a47..ec023c69c48 100644 --- a/src/providers/wms/qgswmscapabilities.cpp +++ b/src/providers/wms/qgswmscapabilities.cpp @@ -1793,7 +1793,7 @@ void QgsWmsCapabilities::parseWMTSContents( const QDomElement &element ) tileMatrix.scaleDenom = secondChildElement.firstChildElement( QStringLiteral( "ScaleDenominator" ) ).text().toDouble(); - QStringList topLeft = secondChildElement.firstChildElement( QStringLiteral( "TopLeftCorner" ) ).text().split( ' ' ); + QStringList topLeft = secondChildElement.firstChildElement( QStringLiteral( "TopLeftCorner" ) ).text().split( ' ', Qt::SkipEmptyParts ); if ( topLeft.size() == 2 ) { if ( invert ) @@ -1856,8 +1856,8 @@ void QgsWmsCapabilities::parseWMTSContents( const QDomElement &element ) QDomElement bbox = childElement.firstChildElement( QStringLiteral( "ows:WGS84BoundingBox" ) ); if ( !bbox.isNull() ) { - QStringList ll = bbox.firstChildElement( QStringLiteral( "ows:LowerCorner" ) ).text().split( ' ' ); - QStringList ur = bbox.firstChildElement( QStringLiteral( "ows:UpperCorner" ) ).text().split( ' ' ); + QStringList ll = bbox.firstChildElement( QStringLiteral( "ows:LowerCorner" ) ).text().split( ' ', Qt::SkipEmptyParts ); + QStringList ur = bbox.firstChildElement( QStringLiteral( "ows:UpperCorner" ) ).text().split( ' ', Qt::SkipEmptyParts ); if ( ll.size() == 2 && ur.size() == 2 ) { @@ -1872,8 +1872,8 @@ void QgsWmsCapabilities::parseWMTSContents( const QDomElement &element ) !bbox.isNull(); bbox = bbox.nextSiblingElement( QStringLiteral( "ows:BoundingBox" ) ) ) { - QStringList ll = bbox.firstChildElement( QStringLiteral( "ows:LowerCorner" ) ).text().split( ' ' ); - QStringList ur = bbox.firstChildElement( QStringLiteral( "ows:UpperCorner" ) ).text().split( ' ' ); + QStringList ll = bbox.firstChildElement( QStringLiteral( "ows:LowerCorner" ) ).text().split( ' ', Qt::SkipEmptyParts ); + QStringList ur = bbox.firstChildElement( QStringLiteral( "ows:UpperCorner" ) ).text().split( ' ', Qt::SkipEmptyParts ); if ( ll.size() == 2 && ur.size() == 2 ) { diff --git a/tests/src/providers/testqgswmscapabilities.cpp b/tests/src/providers/testqgswmscapabilities.cpp index 653c1ff2867..d9c1e1ff83a 100644 --- a/tests/src/providers/testqgswmscapabilities.cpp +++ b/tests/src/providers/testqgswmscapabilities.cpp @@ -596,6 +596,44 @@ class TestQgsWmsCapabilities : public QObject QCOMPARE( res.end(), range.end() ); QCOMPARE( static_cast( resFormat ), format ); } + + void wmtsWithRedundantSpaces() + { + QgsWmsCapabilities capabilities; + const QgsWmsParserSettings config; + + // Note: Redundant spaces in the coordinate tuples + const QByteArray configData( R"""( + + + Layer1 + + 109.999000 -45.081000 + 155.005000 -9.978000 + + + + WholeWorld_CRS_84 + urn:ogc:def:crs:OGC:1.3:CRS84 + urn:ogc:def:wkss:OGC:1.0:GlobalCRS84Pixel + + 1g + 397569609.975977 + -180 90 + 320 + 200 + 2 + 1 + + + +)""" ); + + QVERIFY( capabilities.parseResponse( configData, config ) ); + QCOMPARE( capabilities.supportedTileMatrixSets().size(), 1 ); + QgsWmtsTileLayer layer = capabilities.supportedTileLayers().at( 0 ); + QCOMPARE( layer.boundingBoxes.first().box, QgsRectangle( 109.999, -45.081, 155.005, -9.978 ) ); + } }; QGSTEST_MAIN( TestQgsWmsCapabilities ) From e58b858ad974123784601c63e63238951d76088a Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Thu, 22 May 2025 11:52:47 +0200 Subject: [PATCH 05/14] Remove date fields from graduated classification Fix #61767 --- src/gui/symbology/qgsgraduatedsymbolrendererwidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/symbology/qgsgraduatedsymbolrendererwidget.cpp b/src/gui/symbology/qgsgraduatedsymbolrendererwidget.cpp index cc1e833e40b..62321700652 100644 --- a/src/gui/symbology/qgsgraduatedsymbolrendererwidget.cpp +++ b/src/gui/symbology/qgsgraduatedsymbolrendererwidget.cpp @@ -498,7 +498,7 @@ QgsGraduatedSymbolRendererWidget::QgsGraduatedSymbolRendererWidget( QgsVectorLay mModel = new QgsGraduatedSymbolRendererModel( this, screen() ); - mExpressionWidget->setFilters( QgsFieldProxyModel::Numeric | QgsFieldProxyModel::Date ); + mExpressionWidget->setFilters( QgsFieldProxyModel::Numeric ); mExpressionWidget->setLayer( mLayer ); btnChangeGraduatedSymbol->setLayer( mLayer ); From b7e6ad2c9754a4e22e028cbd579eaff1a288dc67 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Thu, 22 May 2025 13:49:49 +0200 Subject: [PATCH 06/14] Update qgsogrprovider.cpp --- src/core/providers/ogr/qgsogrprovider.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/providers/ogr/qgsogrprovider.cpp b/src/core/providers/ogr/qgsogrprovider.cpp index 349b240fb10..3ece226d3f0 100644 --- a/src/core/providers/ogr/qgsogrprovider.cpp +++ b/src/core/providers/ogr/qgsogrprovider.cpp @@ -900,8 +900,6 @@ void QgsOgrProvider::loadFields() // If subtype is JSON try to load a feature and check if it's // really an object (rather than something else like an array) // fallback to string. - // Note that homogeneous arrays of numbers and strings are already correctly - // identified by OGR so we don't need to check them here. if ( ( ogrType == OFTString || ogrType == OFTWideString ) && ogrSubType == OFSTJSON ) { QRecursiveMutex *layerMutex = nullptr; From 55e39fee1af735862daab0bc80641df93c9ea720 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 22 May 2025 09:57:25 +1000 Subject: [PATCH 07/14] Don't clutter test reports with debug output from grass tests The test reports should only contain information relevant to test failures, not general debugging output for tests which pass. --- .../providers/grass/testqgsgrassprovider.cpp | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/src/providers/grass/testqgsgrassprovider.cpp b/tests/src/providers/grass/testqgsgrassprovider.cpp index 597fb60410d..0861dd2d8cf 100644 --- a/tests/src/providers/grass/testqgsgrassprovider.cpp +++ b/tests/src/providers/grass/testqgsgrassprovider.cpp @@ -127,13 +127,13 @@ QString TestQgsGrassCommand::toString() const { if ( grassFeature.hasGeometry() ) { - string += "
grass: " + grassFeature.geometry().asWkt( 1 ); + string += "grass: " + grassFeature.geometry().asWkt( 1 ); } } if ( expectedFeature.hasGeometry() ) { - string += "
expected: " + expectedFeature.geometry().asWkt( 1 ); + string += "expected: " + expectedFeature.geometry().asWkt( 1 ); } } else if ( command == DeleteFeature ) @@ -243,11 +243,16 @@ class TestQgsGrassProvider : public QgsTest void TestQgsGrassProvider::reportRow( const QString &message ) { - mReport += message + "
\n"; + for ( const QString &line : message.split( '\n' ) ) + { + qDebug() << line; + } } void TestQgsGrassProvider::reportHeader( const QString &message ) { - mReport += "

" + message + "

\n"; + qDebug() << "------"; + qDebug() << message; + qDebug() << "------"; } //runs before all tests @@ -264,8 +269,8 @@ void TestQgsGrassProvider::initTestCase() QgsApplication::initQgis(); QString mySettings = QgsApplication::showSettings(); mySettings = mySettings.replace( QLatin1String( "\n" ), QLatin1String( "
\n" ) ); - mReport += QStringLiteral( "

GRASS %1 provider tests

\n" ).arg( GRASS_BUILD_VERSION ); - mReport += "

" + mySettings + "

\n"; + reportHeader( QStringLiteral( "

GRASS %1 provider tests

\n" ).arg( GRASS_BUILD_VERSION ) ); + reportRow( mySettings ); #ifndef Q_OS_WIN reportRow( "LD_LIBRARY_PATH: " + QString( getenv( "LD_LIBRARY_PATH" ) ) ); @@ -1163,7 +1168,7 @@ void TestQgsGrassProvider::edit() Q_FOREACH ( const TestQgsGrassCommand &command, commandGroup.commands ) { - reportRow( "
command: " + command.toString() ); + reportRow( "command: " + command.toString() ); bool commandOk = true; if ( command.command == TestQgsGrassCommand::StartEditing ) @@ -1388,7 +1393,7 @@ void TestQgsGrassProvider::edit() { for ( int j = editCommands.size() - 1; j >= 0; j-- ) { - reportRow( "
undo command: " + editCommands[j].toString() ); + reportRow( "undo command: " + editCommands[j].toString() ); grassLayer->undoStack()->undo(); expectedLayer->undoStack()->undo(); if ( !compare( expectedLayers, ok ) ) @@ -1418,7 +1423,7 @@ void TestQgsGrassProvider::edit() { for ( int j = 0; j < editCommands.size(); j++ ) { - reportRow( "
redo command: " + editCommands[j].toString() ); + reportRow( "redo command: " + editCommands[j].toString() ); grassLayer->undoStack()->redo(); expectedLayer->undoStack()->redo(); if ( !compare( expectedLayers, ok ) ) From 6ef35ccd3e7004116e9b94ba2689d477fefd4709 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 22 May 2025 09:58:11 +1000 Subject: [PATCH 08/14] Improve error message --- src/providers/grass/qgsgrassprovider.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/grass/qgsgrassprovider.cpp b/src/providers/grass/qgsgrassprovider.cpp index 45c9fbf2679..09ce2b7cc4c 100644 --- a/src/providers/grass/qgsgrassprovider.cpp +++ b/src/providers/grass/qgsgrassprovider.cpp @@ -166,7 +166,7 @@ QgsGrassProvider::QgsGrassProvider( const QString &uri ) mLayerField = grassLayer( mLayerName ); if ( mLayerField == -1 ) { - QgsDebugError( QString( "Invalid layer name, no underscore found: %1" ).arg( mLayerName ) ); + QgsDebugError( QString( "Invalid layer name, no underscore found: \"%1\"" ).arg( mLayerName ) ); return; } From 52e71951ee58ce52895633a3fb48613c7caa5f9d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 22 May 2025 09:58:26 +1000 Subject: [PATCH 09/14] Fix error reported in wrong branch --- src/providers/grass/qgsgrassvectormaplayer.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/providers/grass/qgsgrassvectormaplayer.cpp b/src/providers/grass/qgsgrassvectormaplayer.cpp index f169c70152e..e96867ef168 100644 --- a/src/providers/grass/qgsgrassvectormaplayer.cpp +++ b/src/providers/grass/qgsgrassvectormaplayer.cpp @@ -777,13 +777,16 @@ void QgsGrassVectorMapLayer::deleteColumn( const QgsField &field, QString &error if ( error.isEmpty() ) { - QgsDebugError( "error = " + error ); int index = mTableFields.indexFromName( field.name() ); if ( index != -1 ) { mTableFields.remove( index ); } } + else + { + QgsDebugError( "error = " + error ); + } } void QgsGrassVectorMapLayer::insertCats( QString &error ) From d6c8e2e7a700751ece7d2af14eafe565ae6782ed Mon Sep 17 00:00:00 2001 From: Malik Blesius <78353871+mblesius@users.noreply.github.com> Date: Fri, 23 May 2025 11:20:30 +0200 Subject: [PATCH 10/14] swap '$name' and 'function' and replace '
' with '\n' For QgsPyExpressionFunctions swap the positions of the '$name'-placeholder and 'function' to be consistent with native QGIS functions. Also replace the following break-Tag with a newline to get rid of extra space. --- python/PyQt6/core/additions/qgsfunction.py | 2 +- python/core/additions/qgsfunction.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/PyQt6/core/additions/qgsfunction.py b/python/PyQt6/core/additions/qgsfunction.py index 32a338dac29..2393c9c1f66 100644 --- a/python/PyQt6/core/additions/qgsfunction.py +++ b/python/PyQt6/core/additions/qgsfunction.py @@ -138,7 +138,7 @@ def register_function( """ # Format the help text - helptemplate = string.Template("

$name function


$doc") + helptemplate = string.Template("

function $name

\n$doc") name = kwargs.get("name", function.__name__) helptext = kwargs.get("helpText") or function.__doc__ or "" helptext = helptext.strip() diff --git a/python/core/additions/qgsfunction.py b/python/core/additions/qgsfunction.py index 32a338dac29..2393c9c1f66 100644 --- a/python/core/additions/qgsfunction.py +++ b/python/core/additions/qgsfunction.py @@ -138,7 +138,7 @@ def register_function( """ # Format the help text - helptemplate = string.Template("

$name function


$doc") + helptemplate = string.Template("

function $name

\n$doc") name = kwargs.get("name", function.__name__) helptext = kwargs.get("helpText") or function.__doc__ or "" helptext = helptext.strip() From 0c488cc71848b058691a1300acfa593c4b29e4ff Mon Sep 17 00:00:00 2001 From: Malik Blesius <78353871+mblesius@users.noreply.github.com> Date: Fri, 23 May 2025 16:19:11 +0200 Subject: [PATCH 11/14] Update test_qgsexpression.py Update test_qgsexpression.py for new help text template --- tests/src/python/test_qgsexpression.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/src/python/test_qgsexpression.py b/tests/src/python/test_qgsexpression.py index 49041040b5b..eb5cf56ff4e 100644 --- a/tests/src/python/test_qgsexpression.py +++ b/tests/src/python/test_qgsexpression.py @@ -120,13 +120,13 @@ class TestQgsExpressionCustomFunctions(unittest.TestCase): """Test help about python function.""" QgsExpression.registerFunction(self.help_with_variable) html = ( - "

help_with_variable function


" "The help comes from a variable." + "

function help_with_variable

\n" "The help comes from a variable." ) self.assertEqual(self.help_with_variable.helpText(), html) QgsExpression.registerFunction(self.help_with_docstring) html = ( - "

help_with_docstring function


" + "

function help_with_docstring

\n" "The help comes from the python docstring." ) self.assertEqual(self.help_with_docstring.helpText(), html) From 1728e5762809c786aed59b43d57d1777892bea2c Mon Sep 17 00:00:00 2001 From: "Juergen E. Fischer" Date: Fri, 23 May 2025 20:06:10 +0200 Subject: [PATCH 12/14] also install nlohmann/detail/abi_macros.hpp (followup 5aa6938817fd7db477abd39901bccd9349561d03) --- src/core/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6219369386b..a7e629e318d 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -2495,6 +2495,7 @@ if(NOT APPLE OR NOT QGIS_MACAPP_FRAMEWORK) if(WITH_INTERNAL_NLOHMANN_JSON) install(FILES ${CMAKE_SOURCE_DIR}/external/nlohmann/json_fwd.hpp DESTINATION ${QGIS_INCLUDE_DIR}/nlohmann) + install(FILES ${CMAKE_SOURCE_DIR}/external/nlohmann/detail/abi_macros.hpp DESTINATION ${QGIS_INCLUDE_DIR}/nlohmann/detail) endif() else() From fcf2a6de3ef97a4d87c5c37e2107ea53fdb8b168 Mon Sep 17 00:00:00 2001 From: bdm-oslandia Date: Mon, 26 May 2025 16:11:41 +0200 Subject: [PATCH 13/14] fix(QgsAuxiliaryStorage): remove tmp file only if it exists --- src/core/qgsauxiliarystorage.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/qgsauxiliarystorage.cpp b/src/core/qgsauxiliarystorage.cpp index 9ad79a40654..c9852d25dbf 100644 --- a/src/core/qgsauxiliarystorage.cpp +++ b/src/core/qgsauxiliarystorage.cpp @@ -635,7 +635,8 @@ QgsAuxiliaryStorage::QgsAuxiliaryStorage( const QString &filename, bool copy ) QgsAuxiliaryStorage::~QgsAuxiliaryStorage() { - QFile::remove( mTmpFileName ); + if ( QFile::exists( mTmpFileName ) ) + QFile::remove( mTmpFileName ); } bool QgsAuxiliaryStorage::isValid() const From 1111d01007ea36255faed0e7c7220cce20bb98b6 Mon Sep 17 00:00:00 2001 From: "Juergen E. Fischer" Date: Fri, 23 May 2025 21:49:54 +0200 Subject: [PATCH 14/14] debian packaging: add trixie and plucky --- debian/rules | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/rules b/debian/rules index 2540cfcf1cb..8a8038f1282 100755 --- a/debian/rules +++ b/debian/rules @@ -43,7 +43,7 @@ endif QT_PLUGINS_DIR = lib/$(DEB_BUILD_MULTIARCH)/qt5/plugins -ifneq ($(DISTRIBUTION),$(findstring $(DISTRIBUTION),"bookworm jammy kinetic lunar mantic noble oracular")) +ifneq ($(DISTRIBUTION),$(findstring $(DISTRIBUTION),"bookworm trixie jammy kinetic lunar mantic noble oracular plucky")) DISTRIBUTION := sid endif @@ -115,7 +115,7 @@ ifneq (,$(findstring ;$(GRASSVER);, ";7;8;")) -DGRASS_PREFIX$(GRASSVER)=/usr/lib/$(GRASS) endif -ifneq (,$(findstring $(DISTRIBUTION),"sid kinetic lunar mantic noble oracular")) +ifneq (,$(findstring $(DISTRIBUTION),"trixie sid kinetic lunar mantic noble oracular plucky")) CMAKE_OPTS += -DGDAL_LIBRARY=/usr/lib/$(DEB_BUILD_MULTIARCH)/libgdal.so endif