Skip to content

Commit

Permalink
JSON fields: logic to guess widget type
Browse files Browse the repository at this point in the history
Fix #57673
  • Loading branch information
elpaso committed Jun 7, 2024
1 parent 998744e commit 483bed0
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 2 deletions.
36 changes: 36 additions & 0 deletions src/gui/editorwidgets/qgskeyvaluewidgetfactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,41 @@ QgsEditorConfigWidget *QgsKeyValueWidgetFactory::configWidget( QgsVectorLayer *v
unsigned int QgsKeyValueWidgetFactory::fieldScore( const QgsVectorLayer *vl, int fieldIdx ) const
{
const QgsField field = vl->fields().field( fieldIdx );
if ( field.type() == QMetaType::Type::QVariantMap && ( field.typeName().compare( QStringLiteral( "JSON" ), Qt::CaseSensitivity::CaseInsensitive ) == 0 || field.subType() == QMetaType::Type::QString ) )
{
// Fetch the first not-null value and check if it is really a map
QgsFeatureRequest req;
req.setFlags( Qgis::FeatureRequestFlag::NoGeometry );
req.setSubsetOfAttributes( { fieldIdx } );
req.setFilterExpression( QStringLiteral( R"("%1" IS NOT NULL)" ).arg( field.name() ) );
req.setLimit( 1 );
QgsFeature f;
QgsFeatureIterator featureIt { vl->getFeatures( req ) };
if ( featureIt.nextFeature( f ) )
{
// Get attribute value and check if it is a valid JSON object
const QVariant value { f.attribute( fieldIdx ) };
switch ( value.type() )
{
case QVariant::Type::Map:
{
return 20;
}
default:
case QVariant::Type::String:
{
const QJsonDocument doc = QJsonDocument::fromJson( value.toString().toUtf8() );
if ( doc.isObject() )
{
return 20;
}
else
{
return 0;
}
}
}
}
}
return field.type() == QMetaType::Type::QVariantMap && field.subType() != QMetaType::Type::UnknownType ? 20 : 0;
}
37 changes: 37 additions & 0 deletions src/gui/editorwidgets/qgslistwidgetfactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,42 @@ QgsEditorConfigWidget *QgsListWidgetFactory::configWidget( QgsVectorLayer *vl, i
unsigned int QgsListWidgetFactory::fieldScore( const QgsVectorLayer *vl, int fieldIdx ) const
{
const QgsField field = vl->fields().field( fieldIdx );
// Check if this is a JSON field misinterpreted as a map
if ( field.type() == QMetaType::Type::QVariantMap && ( field.typeName().compare( QStringLiteral( "JSON" ), Qt::CaseSensitivity::CaseInsensitive ) == 0 || field.subType() == QMetaType::Type::QString ) )
{
// Fetch the first not-null value and check if it is really an array
QgsFeatureRequest req;
req.setFlags( Qgis::FeatureRequestFlag::NoGeometry );
req.setSubsetOfAttributes( { fieldIdx } );
req.setFilterExpression( QStringLiteral( R"("%1" IS NOT NULL)" ).arg( field.name() ) );
req.setLimit( 1 );
QgsFeature f;
QgsFeatureIterator featureIt { vl->getFeatures( req ) };
if ( featureIt.nextFeature( f ) )
{
// Get attribute value and check if it is a valid JSON object
const QVariant value { f.attribute( fieldIdx ) };
switch ( value.type() )
{
case QVariant::Type::List:
{
return 20;
}
default:
case QVariant::Type::String:
{
const QJsonDocument doc = QJsonDocument::fromJson( value.toString().toUtf8() );
if ( doc.isArray() )
{
return 20;
}
else
{
return 0;
}
}
}
}
}
return ( field.type() == QMetaType::Type::QVariantList || field.type() == QMetaType::Type::QStringList || field.type() == QMetaType::Type::QVariantMap ) && field.subType() != QMetaType::Type::UnknownType ? 20 : 0;
}
102 changes: 100 additions & 2 deletions tests/src/python/test_qgsattributeformeditorwidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
__date__ = '2016-05'
__copyright__ = 'Copyright 2016, The QGIS Project'

from qgis.PyQt.QtCore import QDate, QDateTime, QTime
from qgis.PyQt.QtCore import QDate, QDateTime, QTime, QVariant, QTemporaryDir
from qgis.PyQt.QtWidgets import QDateTimeEdit, QWidget
from qgis.core import QgsVectorLayer
from qgis.core import QgsVectorLayer, QgsField, QgsFeature
from qgis.gui import (
QgsAttributeForm,
QgsAttributeFormEditorWidget,
Expand All @@ -22,6 +22,7 @@
)
import unittest
from qgis.testing import start_app, QgisTestCase
from osgeo import gdal, ogr, osr

start_app()
QgsGui.editorWidgetRegistry().initEditors()
Expand Down Expand Up @@ -99,6 +100,103 @@ def testBetweenFilter(self):
sb.setActiveFlags(QgsSearchWidgetWrapper.FilterFlag.IsNotBetween)
self.assertEqual(af.currentFilterExpression(), '"fldtext"<\'2013-05-06\' OR "fldtext">\'2013-05-16\'')

def verifyJSONTypeFindBest(self, field_type, layer):

# Create a JSON field
field = QgsField('json', field_type, 'JSON', 0, 0, 'comment', QVariant.String)
self.assertTrue(layer.startEditing())
self.assertTrue(layer.addAttribute(field))
self.assertTrue(layer.commitChanges())

# No records, so should default to key/value
registry = QgsGui.editorWidgetRegistry()
setup = registry.findBest(layer, 'json')
self.assertEqual(setup.type(), 'KeyValue')

# Add a key/value record
layer.startEditing()
feature = QgsFeature(layer.fields())
feature.setAttribute('json', '{"key": "value"}')
self.assertTrue(layer.addFeature(feature))
setup = registry.findBest(layer, 'json')
self.assertEqual(setup.type(), 'KeyValue')
layer.rollBack()

# Add an array record
self.assertTrue(layer.startEditing())
feature = QgsFeature(layer.fields())
feature.setAttribute('json', '["value", "another_value"]')
self.assertTrue(layer.addFeature(feature))
setup = registry.findBest(layer, 'json')
self.assertEqual(setup.type(), 'List')
self.assertTrue(layer.rollBack())

# Add a null record followed by a map record followed by a list record
self.assertTrue(layer.startEditing())
feature = QgsFeature(layer.fields())
feature.setAttribute('json', None)
self.assertTrue(layer.addFeature(feature))
feature = QgsFeature(layer.fields())
feature.setAttribute('json', '{"key": "value"}')
self.assertTrue(layer.addFeature(feature))
feature = QgsFeature(layer.fields())
feature.setAttribute('json', '["value", "another_value"]')
self.assertTrue(layer.addFeature(feature))
setup = registry.findBest(layer, 'json')
self.assertEqual(setup.type(), 'KeyValue')
self.assertTrue(layer.rollBack())

# Add a null record followed by A list record followed by a map record
self.assertTrue(layer.startEditing())
feature = QgsFeature(layer.fields())
feature.setAttribute('json', None)
self.assertTrue(layer.addFeature(feature))
feature = QgsFeature(layer.fields())
feature.setAttribute('json', '["value", "another_value"]')
self.assertTrue(layer.addFeature(feature))
feature = QgsFeature(layer.fields())
feature.setAttribute('json', '{"key": "value"}')
self.assertTrue(layer.addFeature(feature))
setup = registry.findBest(layer, 'json')
self.assertEqual(setup.type(), 'List')
self.assertTrue(layer.rollBack())

# Add a string record which is neither a list or a map
self.assertTrue(layer.startEditing())
feature = QgsFeature(layer.fields())
feature.setAttribute('json', 'not a list or map')
self.assertTrue(layer.addFeature(feature))
setup = registry.findBest(layer, 'json')
self.assertNotEqual(setup.type(), 'List')
self.assertNotEqual(setup.type(), 'KeyValue')
self.assertTrue(layer.rollBack())

# Cleanup removing the field
self.assertTrue(layer.startEditing())
field_idx = layer.fields().indexOf('json')
self.assertTrue(layer.deleteAttribute(field_idx))
self.assertTrue(layer.commitChanges())

def testJSONMemoryLayer(self):

layer = QgsVectorLayer("Point?", "test", "memory")
self.verifyJSONTypeFindBest(QVariant.Map, layer)

def testJSONGeoPackageLayer(self):

temp_dir = QTemporaryDir()
uri = temp_dir.filePath("test.gpkg")
# Create a new geopackage layer using ogr
driver = ogr.GetDriverByName('GPKG')
ds = driver.CreateDataSource(uri)
srs = osr.SpatialReference()
srs.ImportFromEPSG(4326)
layer = ds.CreateLayer('test', srs, ogr.wkbPoint)
del layer
del ds
layer = QgsVectorLayer(uri, 'test', 'ogr')
self.verifyJSONTypeFindBest(QVariant.Map, layer)


if __name__ == '__main__':
unittest.main()

0 comments on commit 483bed0

Please sign in to comment.