From 83328ae5962816bca5d6728c18a034357e346618 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Mon, 12 Mar 2018 19:20:26 +0100 Subject: [PATCH 01/13] [feature] Form context expressions in value relation widget The value relation widget filter expression can now use two new functions/variables that have access to the current values and geometry of the form being edited. This allows for dynamic filtering (drill-down) as explained in the crowdfunding page: https://north-road.com/drill-down-cascading-forms/ The new functions/variables are: Function: get_current_form_field_value( 'FIELD_NAME' ) Variable: @current_form_geometry --- .../qgsvaluerelationfieldformatter.sip.in | 70 ++++++- .../qgsexpressioncontext.sip.in | 8 + .../core/qgseditorwidgetwrapper.sip.in | 4 + .../qgsattributeeditorcontext.sip.in | 19 ++ .../auto_generated/qgsattributeform.sip.in | 3 +- .../json/get_current_form_field_value.txt | 7 + src/core/expression/qgsexpression.cpp | 3 + .../qgsvaluerelationfieldformatter.cpp | 71 ++++++- .../qgsvaluerelationfieldformatter.h | 69 ++++++- src/core/qgsexpressioncontext.cpp | 39 ++++ src/core/qgsexpressioncontext.h | 23 ++- .../qgsattributetabledelegate.cpp | 6 +- .../attributetable/qgsattributetablemodel.h | 2 +- .../core/qgseditorwidgetwrapper.h | 14 +- .../qgsvaluerelationconfigdlg.cpp | 1 + .../qgsvaluerelationsearchwidgetwrapper.cpp | 1 - .../qgsvaluerelationwidgetwrapper.cpp | 193 +++++++++++++----- .../qgsvaluerelationwidgetwrapper.h | 31 +++ src/gui/qgsattributeeditorcontext.h | 18 ++ src/gui/qgsattributeform.cpp | 1 + src/gui/qgsattributeform.h | 5 +- .../gui/testqgsvaluerelationwidgetwrapper.cpp | 82 ++++++++ tests/src/python/test_qgsfieldformatters.py | 36 +++- 23 files changed, 615 insertions(+), 91 deletions(-) create mode 100644 resources/function_help/json/get_current_form_field_value.txt diff --git a/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in b/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in index e9a1d00d09a..f8e91857944 100644 --- a/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in +++ b/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in @@ -8,6 +8,7 @@ + class QgsValueRelationFieldFormatter : QgsFieldFormatter { %Docstring @@ -56,15 +57,6 @@ Constructor for QgsValueRelationFieldFormatter. virtual QVariant createCache( QgsVectorLayer *layer, int fieldIndex, const QVariantMap &config ) const; - static QgsValueRelationFieldFormatter::ValueRelationCache createCache( const QVariantMap &config ); -%Docstring -Create a cache for a value relation field. -This can be used to keep the value map in the local memory -if doing multiple lookups in a loop. - -.. versionadded:: 3.0 -%End - static QStringList valueToStringList( const QVariant &value ); %Docstring Utility to convert an array or a string representation of and array ``value`` to a string list @@ -75,6 +67,66 @@ Utility to convert an array or a string representation of and array ``value`` to .. versionadded:: 3.2 %End + + static QgsValueRelationFieldFormatter::ValueRelationCache createCache( const QVariantMap &config, const QgsFeature &formFeature = QgsFeature() ); +%Docstring +Create a cache for a value relation field. +This can be used to keep the value map in the local memory +if doing multiple lookups in a loop. + +:param config: The widget configuration +:param formFeature: The feature currently being edited with current attribute values + +:return: A kvp list of values for the widget + +.. versionadded:: 3.0 +%End + + static bool expressionRequiresFormScope( const QString &expression ); +%Docstring +Check if the ``expression`` requires a form scope (i.e. if it uses fields +or geometry of the currently edited feature). + +:param expression: The widget's filter expression + +:return: true if the expression requires a form scope + +.. versionadded:: 3.2 +%End + + static QSet expressionFormAttributes( const QString &expression ); +%Docstring +Return a list of attributes required by the form context ``expression`` + +:param expression: Form filter expression + +:return: list of attributes required by the expression + +.. versionadded:: 3.2 +%End + + static QSet expressionFormVariables( const QString &expression ); +%Docstring +Return a list of variables required by the form context ``expression`` + +:param expression: Form filter expression + +:return: list of variables required by the expression + +.. versionadded:: 3.2 +%End + + static bool expressionIsUsable( const QString &expression, const QgsFeature &feature ); +%Docstring +Check wether the ``feature`` has all values required by the ``expression`` + +@return True if the expression can be used + +.. versionadded:: 3.2 +%End + + static QString FORM_SCOPE_FUNCTIONS_RE; + }; diff --git a/python/core/auto_generated/qgsexpressioncontext.sip.in b/python/core/auto_generated/qgsexpressioncontext.sip.in index b71e6c30819..c4d3d625b9f 100644 --- a/python/core/auto_generated/qgsexpressioncontext.sip.in +++ b/python/core/auto_generated/qgsexpressioncontext.sip.in @@ -763,6 +763,14 @@ Creates a new scope which contains variables and functions relating to the globa For instance, QGIS version numbers and variables specified through QGIS options. .. seealso:: :py:func:`setGlobalVariable` +%End + + static QgsExpressionContextScope *formScope( const QgsFeature &formFeature = QgsFeature( ) ) /Factory/; +%Docstring +Creates a new scope which contains functions and variables from the current attribute form/table feature. +The variables and values in this scope will reflect the current state of the form/row being edited. + +.. versionadded:: 3.2 %End static void setGlobalVariable( const QString &name, const QVariant &value ); diff --git a/python/gui/auto_generated/editorwidgets/core/qgseditorwidgetwrapper.sip.in b/python/gui/auto_generated/editorwidgets/core/qgseditorwidgetwrapper.sip.in index bb160fa6a24..355ce4ee247 100644 --- a/python/gui/auto_generated/editorwidgets/core/qgseditorwidgetwrapper.sip.in +++ b/python/gui/auto_generated/editorwidgets/core/qgseditorwidgetwrapper.sip.in @@ -279,6 +279,10 @@ change the visual cue. .. versionadded:: 2.16 %End + protected: + + + }; diff --git a/python/gui/auto_generated/qgsattributeeditorcontext.sip.in b/python/gui/auto_generated/qgsattributeeditorcontext.sip.in index 133ba28f88d..759952b6e3f 100644 --- a/python/gui/auto_generated/qgsattributeeditorcontext.sip.in +++ b/python/gui/auto_generated/qgsattributeeditorcontext.sip.in @@ -180,6 +180,25 @@ QGIS forms const QgsAttributeEditorContext *parentContext() const; + const QgsFeature formFeature() const; +%Docstring +Return current feature from the currently edited form or table row + +.. seealso:: :py:func:`setFormFeature` + +.. versionadded:: 3.2 +%End + + void setFormFeature( const QgsFeature &feature ); +%Docstring +Set current ``feature`` for the currently edited form or table row + +.. seealso:: :py:func:`formFeature` + +.. versionadded:: 3.2 +%End + + }; /************************************************************************ diff --git a/python/gui/auto_generated/qgsattributeform.sip.in b/python/gui/auto_generated/qgsattributeform.sip.in index e5a2dcf2eae..e4fd845302a 100644 --- a/python/gui/auto_generated/qgsattributeform.sip.in +++ b/python/gui/auto_generated/qgsattributeform.sip.in @@ -141,7 +141,8 @@ on all attribute widgets. void attributeChanged( const QString &attribute, const QVariant &value ) /Deprecated/; %Docstring -Notifies about changes of attributes +Notifies about changes of attributes, this signal is not emitted when the value is set +back to the original one. :param attribute: The name of the attribute that changed. :param value: The new value of the attribute. diff --git a/resources/function_help/json/get_current_form_field_value.txt b/resources/function_help/json/get_current_form_field_value.txt new file mode 100644 index 00000000000..19b1c809e56 --- /dev/null +++ b/resources/function_help/json/get_current_form_field_value.txt @@ -0,0 +1,7 @@ +{ + "name": "get_current_form_field_value", + "type": "function", + "description": "Returns the current value of a field in the form or table row currently being edited.", + "arguments": [ {"arg":"field_name","description":"a field name in the current form or table row"}], + "examples": [ { "expression":"get_current_form_field_value( 'FIELD_NAME' )","returns":"The current value of field 'FIELD_NAME'."} ] +} diff --git a/src/core/expression/qgsexpression.cpp b/src/core/expression/qgsexpression.cpp index fae46ad823a..85aec1a6757 100644 --- a/src/core/expression/qgsexpression.cpp +++ b/src/core/expression/qgsexpression.cpp @@ -771,6 +771,9 @@ void QgsExpression::initVariableHelp() //provider notification sVariableHelpTexts.insert( QStringLiteral( "notification_message" ), QCoreApplication::translate( "notification_message", "Content of the notification message sent by the provider (available only for actions triggered by provider notifications)." ) ); + + //form context variable + sVariableHelpTexts.insert( QStringLiteral( "current_form_geometry" ), QCoreApplication::translate( "current_form_geometry", "Represents the geometry of the feature currently being edited in the form or the table row. Can be used for in a form/row context to filter the related features." ) ); } QString QgsExpression::variableHelpText( const QString &variableName ) diff --git a/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp b/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp index 4df6a75c08b..2e7e8a0a526 100644 --- a/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp +++ b/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp @@ -21,6 +21,8 @@ #include +QString QgsValueRelationFieldFormatter::FORM_SCOPE_FUNCTIONS_RE = QStringLiteral( "%1\\s*\\(\\s*'([^']+)'\\s*\\)" ); + bool orderByKeyLessThan( const QgsValueRelationFieldFormatter::ValueRelationItem &p1, const QgsValueRelationFieldFormatter::ValueRelationItem &p2 ) { return qgsVariantLessThan( p1.key, p2.key ); @@ -99,7 +101,7 @@ QVariant QgsValueRelationFieldFormatter::createCache( QgsVectorLayer *layer, int } -QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatter::createCache( const QVariantMap &config ) +QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatter::createCache( const QVariantMap &config, const QgsFeature &formFeature ) { ValueRelationCache cache; @@ -116,11 +118,19 @@ QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatte request.setFlags( QgsFeatureRequest::NoGeometry ); request.setSubsetOfAttributes( QgsAttributeList() << ki << vi ); - if ( !config.value( QStringLiteral( "FilterExpression" ) ).toString().isEmpty() ) + + const QString expression = config.value( QStringLiteral( "FilterExpression" ) ).toString(); + + // Skip the filter and build a full cache if the form scope is required and the feature + // is not valid or the attributes required for the filter have no valid value + if ( ! expression.isEmpty() && ( ! expressionRequiresFormScope( expression ) + || expressionIsUsable( expression, formFeature ) ) ) { QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) ); + if ( formFeature.isValid( ) ) + context.appendScope( QgsExpressionContextUtils::formScope( formFeature ) ); request.setExpressionContext( context ); - request.setFilterExpression( config.value( QStringLiteral( "FilterExpression" ) ).toString() ); + request.setFilterExpression( expression ); } QgsFeatureIterator fit = layer->getFeatures( request ); @@ -162,3 +172,58 @@ QStringList QgsValueRelationFieldFormatter::valueToStringList( const QVariant &v } return checkList; } + + +QSet QgsValueRelationFieldFormatter::expressionFormVariables( const QString &expression ) +{ + const QStringList formVariables( QgsExpressionContextUtils::formScope()->variableNames() ); + QSet variables; + + for ( auto it = formVariables.constBegin(); it != formVariables.constEnd(); it++ ) + { + if ( expression.contains( *it ) ) + { + variables.insert( *it ); + } + } + return variables; +} + +bool QgsValueRelationFieldFormatter::expressionRequiresFormScope( const QString &expression ) +{ + return !( expressionFormAttributes( expression ).isEmpty() && expressionFormVariables( expression ).isEmpty() ); +} + +QSet QgsValueRelationFieldFormatter::expressionFormAttributes( const QString &expression ) +{ + QSet attributes; + const QStringList formFunctions( QgsExpressionContextUtils::formScope()->functionNames() ); + QRegularExpression re; + for ( const auto &fname : formFunctions ) + { + if ( QgsExpressionContextUtils::formScope()->function( fname )->parameters().count( ) != 0 ) + { + re.setPattern( QgsValueRelationFieldFormatter::FORM_SCOPE_FUNCTIONS_RE.arg( fname ) ); + QRegularExpressionMatchIterator i = re.globalMatch( expression ); + while ( i.hasNext() ) + { + QRegularExpressionMatch match = i.next(); + attributes.insert( match.captured( 1 ) ); + } + } + } + return attributes; +} + +bool QgsValueRelationFieldFormatter::expressionIsUsable( const QString &expression, const QgsFeature &feature ) +{ + const QSet attrs = expressionFormAttributes( expression ); + for ( auto it = attrs.constBegin() ; it != attrs.constEnd(); it++ ) + { + if ( ! feature.attribute( *it ).isValid() ) + return false; + } + if ( ! expressionFormVariables( expression ).isEmpty() && feature.geometry().isEmpty( ) ) + return false; + return true; +} diff --git a/src/core/fieldformatter/qgsvaluerelationfieldformatter.h b/src/core/fieldformatter/qgsvaluerelationfieldformatter.h index 9840b09d76d..3ba24b11e80 100644 --- a/src/core/fieldformatter/qgsvaluerelationfieldformatter.h +++ b/src/core/fieldformatter/qgsvaluerelationfieldformatter.h @@ -18,10 +18,13 @@ #include "qgis_core.h" #include "qgsfieldformatter.h" +#include "qgsexpression.h" +#include "qgsexpressioncontext.h" #include #include + /** * \ingroup core * Field formatter for a value relation field. @@ -62,15 +65,6 @@ class CORE_EXPORT QgsValueRelationFieldFormatter : public QgsFieldFormatter QVariant createCache( QgsVectorLayer *layer, int fieldIndex, const QVariantMap &config ) const override; - /** - * Create a cache for a value relation field. - * This can be used to keep the value map in the local memory - * if doing multiple lookups in a loop. - * - * \since QGIS 3.0 - */ - static QgsValueRelationFieldFormatter::ValueRelationCache createCache( const QVariantMap &config ); - /** * Utility to convert an array or a string representation of and array \a value to a string list * @@ -79,6 +73,63 @@ class CORE_EXPORT QgsValueRelationFieldFormatter : public QgsFieldFormatter * \since QGIS 3.2 */ static QStringList valueToStringList( const QVariant &value ); + + /** + * Create a cache for a value relation field. + * This can be used to keep the value map in the local memory + * if doing multiple lookups in a loop. + * \param config The widget configuration + * \param formFeature The feature currently being edited with current attribute values + * \return A kvp list of values for the widget + * + * \since QGIS 3.0 + */ + static QgsValueRelationFieldFormatter::ValueRelationCache createCache( const QVariantMap &config, const QgsFeature &formFeature = QgsFeature() ); + + /** + * Check if the \a expression requires a form scope (i.e. if it uses fields + * or geometry of the currently edited feature). + * + * \param expression The widget's filter expression + * \return true if the expression requires a form scope + * \since QGIS 3.2 + */ + static bool expressionRequiresFormScope( const QString &expression ); + + /** + * Return a list of attributes required by the form context \a expression + * + * \param expression Form filter expression + * \return list of attributes required by the expression + * \since QGIS 3.2 + */ + static QSet expressionFormAttributes( const QString &expression ); + + /** + * Return a list of variables required by the form context \a expression + * + * \param expression Form filter expression + * \return list of variables required by the expression + * \since QGIS 3.2 + */ + static QSet expressionFormVariables( const QString &expression ); + + /** + * Check wether the \a feature has all values required by the \a expression + * + * @return True if the expression can be used + * \since QGIS 3.2 + */ + static bool expressionIsUsable( const QString &expression, const QgsFeature &feature ); + + /** + * Regular expression to find dynamic filtering based on form field values + * \see GetCurrentFormFieldValue() + * + * \since QGIS 3.2 + */ + static QString FORM_SCOPE_FUNCTIONS_RE; + }; Q_DECLARE_METATYPE( QgsValueRelationFieldFormatter::ValueRelationCache ) diff --git a/src/core/qgsexpressioncontext.cpp b/src/core/qgsexpressioncontext.cpp index 750e073735b..0c780849770 100644 --- a/src/core/qgsexpressioncontext.cpp +++ b/src/core/qgsexpressioncontext.cpp @@ -736,6 +736,33 @@ class GetLayerVisibility : public QgsScopedExpressionFunction }; + +class GetCurrentFormFieldValue : public QgsScopedExpressionFunction +{ + public: + GetCurrentFormFieldValue( ) + : QgsScopedExpressionFunction( QStringLiteral( "get_current_form_field_value" ), QgsExpressionFunction::ParameterList() << QStringLiteral( "field_name" ), QStringLiteral( "Form" ) ) + {} + + QVariant func( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *, const QgsExpressionNodeFunction * ) override + { + QString fieldName( values.at( 0 ).toString() ); + const QgsFeature feat( context->feature() ); + if ( fieldName.isEmpty() || ! feat.isValid( ) ) + { + return QVariant(); + } + return feat.attribute( fieldName ) ; + } + + QgsScopedExpressionFunction *clone() const override + { + return new GetCurrentFormFieldValue( ); + } + +}; + + class GetProcessingParameterValue : public QgsScopedExpressionFunction { public: @@ -762,6 +789,16 @@ class GetProcessingParameterValue : public QgsScopedExpressionFunction ///@endcond + +QgsExpressionContextScope *QgsExpressionContextUtils::formScope( const QgsFeature &formFeature ) +{ + QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Form" ) ); + scope->setFeature( formFeature ); + scope->addFunction( QStringLiteral( "get_current_form_field_value" ), new GetCurrentFormFieldValue( ) ); + scope->setVariable( QStringLiteral( "current_form_geometry" ), formFeature.geometry( ), true ); + return scope; +} + QgsExpressionContextScope *QgsExpressionContextUtils::projectScope( const QgsProject *project ) { QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Project" ) ); @@ -900,6 +937,7 @@ QList QgsExpressionContextUtils::globalProjectLayer return scopes; } + void QgsExpressionContextUtils::setLayerVariable( QgsMapLayer *layer, const QString &name, const QVariant &value ) { if ( !layer ) @@ -1256,6 +1294,7 @@ void QgsExpressionContextUtils::registerContextFunctions() QgsExpression::registerFunction( new GetLayoutItemVariables( nullptr ) ); QgsExpression::registerFunction( new GetLayerVisibility( QList() ) ); QgsExpression::registerFunction( new GetProcessingParameterValue( QVariantMap() ) ); + QgsExpression::registerFunction( new GetCurrentFormFieldValue( ) ); } bool QgsScopedExpressionFunction::usesGeometry( const QgsExpressionNodeFunction *node ) const diff --git a/src/core/qgsexpressioncontext.h b/src/core/qgsexpressioncontext.h index 1947ebf726b..7da8e2ebca8 100644 --- a/src/core/qgsexpressioncontext.h +++ b/src/core/qgsexpressioncontext.h @@ -739,6 +739,13 @@ class CORE_EXPORT QgsExpressionContextUtils */ static QgsExpressionContextScope *globalScope() SIP_FACTORY; + /** + * Creates a new scope which contains functions and variables from the current attribute form/table feature. + * The variables and values in this scope will reflect the current state of the form/row being edited. + * \since QGIS 3.2 + */ + static QgsExpressionContextScope *formScope( const QgsFeature &formFeature = QgsFeature( ) ) SIP_FACTORY; + /** * Sets a global context variable. This variable will be contained within scopes retrieved via * globalScope(). @@ -823,14 +830,14 @@ class CORE_EXPORT QgsExpressionContextUtils static QList globalProjectLayerScopes( const QgsMapLayer *layer ) SIP_FACTORY; /** - * Sets a layer context variable. This variable will be contained within scopes retrieved via - * layerScope(). - * \param layer map layer - * \param name variable name - * \param value variable value - * \see setLayerVariables() - * \see layerScope() - */ + * Sets a layer context variable. This variable will be contained within scopes retrieved via + * layerScope(). + * \param layer map layer + * \param name variable name + * \param value variable value + * \see setLayerVariables() + * \see layerScope() + */ static void setLayerVariable( QgsMapLayer *layer, const QString &name, const QVariant &value ); /** diff --git a/src/gui/attributetable/qgsattributetabledelegate.cpp b/src/gui/attributetable/qgsattributetabledelegate.cpp index a626c497035..6e0134ac016 100644 --- a/src/gui/attributetable/qgsattributetabledelegate.cpp +++ b/src/gui/attributetable/qgsattributetabledelegate.cpp @@ -67,8 +67,12 @@ QWidget *QgsAttributeTableDelegate::createEditor( QWidget *parent, const QStyleO return nullptr; int fieldIdx = index.model()->data( index, QgsAttributeTableModel::FieldIndexRole ).toInt(); - QgsAttributeEditorContext context( masterModel( index.model() )->editorContext(), QgsAttributeEditorContext::Popup ); + + // Update the editor form context with the feature being edited + QgsFeatureId fid( index.model()->data( index, QgsAttributeTableModel::FeatureIdRole ).toLongLong() ); + context.setFormFeature( vl->getFeature( fid ) ); + QgsEditorWidgetWrapper *eww = QgsGui::editorWidgetRegistry()->create( vl, fieldIdx, nullptr, parent, context ); QWidget *w = eww->widget(); diff --git a/src/gui/attributetable/qgsattributetablemodel.h b/src/gui/attributetable/qgsattributetablemodel.h index ea12259823f..9857f475562 100644 --- a/src/gui/attributetable/qgsattributetablemodel.h +++ b/src/gui/attributetable/qgsattributetablemodel.h @@ -346,7 +346,7 @@ class GUI_EXPORT QgsAttributeTableModel: public QAbstractTableModel mutable QgsExpressionContext mExpressionContext; /** - * Gets mFieldCount, mAttributes and mValueMaps + * Gets mFieldCount, mAttributes */ virtual void loadAttributes(); diff --git a/src/gui/editorwidgets/core/qgseditorwidgetwrapper.h b/src/gui/editorwidgets/core/qgseditorwidgetwrapper.h index e2daa371c6c..588d4dab265 100644 --- a/src/gui/editorwidgets/core/qgseditorwidgetwrapper.h +++ b/src/gui/editorwidgets/core/qgseditorwidgetwrapper.h @@ -277,6 +277,18 @@ class GUI_EXPORT QgsEditorWidgetWrapper : public QgsWidgetWrapper */ virtual void updateConstraintWidgetStatus(); + protected: + + /** + * mFieldIdx the widget feature field id + */ + int mFieldIdx; + + /** + * mFeature the current feature + */ + QgsFeature mFeature; + private: /** @@ -296,8 +308,6 @@ class GUI_EXPORT QgsEditorWidgetWrapper : public QgsWidgetWrapper //! The current constraint result bool mConstraintResultVisible = false; - int mFieldIdx; - QgsFeature mFeature; mutable QVariant mDefaultValue; // Cache default value, we don't want to retrieve different serial numbers if called repeatedly }; diff --git a/src/gui/editorwidgets/qgsvaluerelationconfigdlg.cpp b/src/gui/editorwidgets/qgsvaluerelationconfigdlg.cpp index 2439e8a827c..5429de6584f 100644 --- a/src/gui/editorwidgets/qgsvaluerelationconfigdlg.cpp +++ b/src/gui/editorwidgets/qgsvaluerelationconfigdlg.cpp @@ -92,6 +92,7 @@ void QgsValueRelationConfigDlg::editExpression() return; QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( vl ) ); + context << QgsExpressionContextUtils::formScope( ); QgsExpressionBuilderDialog dlg( vl, mFilterExpression->toPlainText(), this, QStringLiteral( "generic" ), context ); dlg.setWindowTitle( tr( "Edit Filter Expression" ) ); diff --git a/src/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.cpp b/src/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.cpp index 2b23deb23ad..44413049e5b 100644 --- a/src/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgsvaluerelationsearchwidgetwrapper.cpp @@ -161,7 +161,6 @@ void QgsValueRelationSearchWidgetWrapper::onValueChanged() } else { - QgsSettings settings; setExpression( vl.isNull() ? QgsApplication::nullRepresentation() : vl.toString() ); emit valueChanged(); } diff --git a/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp b/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp index b72e29fbf66..e7eec52f859 100644 --- a/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp @@ -23,6 +23,8 @@ #include "qgsfilterlineedit.h" #include "qgsfeatureiterator.h" #include "qgsvaluerelationfieldformatter.h" +#include "qgsattributeform.h" +#include "qgsattributes.h" #include #include @@ -70,7 +72,7 @@ QVariant QgsValueRelationWidgetWrapper::value() const if ( mLineEdit ) { - Q_FOREACH ( const QgsValueRelationFieldFormatter::ValueRelationItem &item, mCache ) + for ( const QgsValueRelationFieldFormatter::ValueRelationItem &item : qgis::as_const( mCache ) ) { if ( item.value == mLineEdit->text() ) { @@ -85,6 +87,12 @@ QVariant QgsValueRelationWidgetWrapper::value() const QWidget *QgsValueRelationWidgetWrapper::createWidget( QWidget *parent ) { + QgsAttributeForm *form = dynamic_cast( parent ); + if ( form ) + connect( form, &QgsAttributeForm::widgetValueChanged, this, &QgsValueRelationWidgetWrapper::widgetValueChanged ); + + mExpression = config().value( QStringLiteral( "FilterExpression" ) ).toString(); + if ( config( QStringLiteral( "AllowMulti" ) ).toBool() ) { return new QTableWidget( parent ); @@ -100,26 +108,18 @@ QWidget *QgsValueRelationWidgetWrapper::createWidget( QWidget *parent ) void QgsValueRelationWidgetWrapper::initWidget( QWidget *editor ) { - mCache = QgsValueRelationFieldFormatter::createCache( config() ); mComboBox = qobject_cast( editor ); mTableWidget = qobject_cast( editor ); mLineEdit = qobject_cast( editor ); + // Read current initial form values from the editor context + setFeature( context().formFeature() ); + if ( mComboBox ) { - if ( config( QStringLiteral( "AllowNull" ) ).toBool() ) - { - mComboBox->addItem( tr( "(no selection)" ), QVariant( field().type() ) ); - } - - Q_FOREACH ( const QgsValueRelationFieldFormatter::ValueRelationItem &element, mCache ) - { - mComboBox->addItem( element.value, element.key ); - } - connect( mComboBox, static_cast( &QComboBox::currentIndexChanged ), - this, static_cast( &QgsEditorWidgetWrapper::emitValueChanged ) ); + this, static_cast( &QgsEditorWidgetWrapper::emitValueChanged ), Qt::UniqueConnection ); } else if ( mTableWidget ) { @@ -130,46 +130,11 @@ void QgsValueRelationWidgetWrapper::initWidget( QWidget *editor ) mTableWidget->setShowGrid( false ); mTableWidget->setEditTriggers( QAbstractItemView::NoEditTriggers ); mTableWidget->setSelectionMode( QAbstractItemView::NoSelection ); - if ( mCache.size() > 0 ) - mTableWidget->setRowCount( ( mCache.size() + config( QStringLiteral( "NofColumns" ) ).toInt() - 1 ) / config( QStringLiteral( "NofColumns" ) ).toInt() ); - else - mTableWidget->setRowCount( 1 ); - if ( config( QStringLiteral( "NofColumns" ) ).toInt() > 0 ) - mTableWidget->setColumnCount( config( QStringLiteral( "NofColumns" ) ).toInt() ); - else - mTableWidget->setColumnCount( 1 ); - - int row = 0, column = 0; - for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : qgis::as_const( mCache ) ) - { - if ( column == config( QStringLiteral( "NofColumns" ) ).toInt() ) - { - row++; - column = 0; - } - QTableWidgetItem *item = nullptr; - item = new QTableWidgetItem( element.value ); - item->setData( Qt::UserRole, element.key ); - mTableWidget->setItem( row, column, item ); - column++; - } - connect( mTableWidget, &QTableWidget::itemChanged, this, static_cast( &QgsEditorWidgetWrapper::emitValueChanged ) ); + connect( mTableWidget, &QTableWidget::itemChanged, this, static_cast( &QgsEditorWidgetWrapper::emitValueChanged ), Qt::UniqueConnection ); } else if ( mLineEdit ) { - QStringList values; - values.reserve( mCache.size() ); - Q_FOREACH ( const QgsValueRelationFieldFormatter::ValueRelationItem &i, mCache ) - { - values << i.value; - } - - QStringListModel *m = new QStringListModel( values, mLineEdit ); - QCompleter *completer = new QCompleter( m, mLineEdit ); - completer->setCaseSensitivity( Qt::CaseInsensitive ); - mLineEdit->setCompleter( completer ); - - connect( mLineEdit, &QLineEdit::textChanged, this, [ = ]( const QString & value ) { emit valueChanged( value ); } ); + connect( mLineEdit, &QLineEdit::textChanged, this, [ = ]( const QString & value ) { emit valueChanged( value ); }, Qt::UniqueConnection ); } } @@ -184,6 +149,11 @@ void QgsValueRelationWidgetWrapper::setValue( const QVariant &value ) { QStringList checkList( QgsValueRelationFieldFormatter::valueToStringList( value ) ); + QTableWidgetItem *lastChangedItem = nullptr; + + // This block is needed because item->setCheckState triggers dataChanged gets back to value() + // and iterate over all items again! This can be extremely slow on large items sets. + mTableWidget->blockSignals( true ); for ( int j = 0; j < mTableWidget->rowCount(); j++ ) { for ( int i = 0; i < config( QStringLiteral( "NofColumns" ) ).toInt() ; ++i ) @@ -192,9 +162,15 @@ void QgsValueRelationWidgetWrapper::setValue( const QVariant &value ) if ( item ) { item->setCheckState( checkList.contains( item->data( Qt::UserRole ).toString() ) ? Qt::Checked : Qt::Unchecked ); + lastChangedItem = item; } } } + mTableWidget->blockSignals( false ); + // let's trigger the signal now, once and for all + if ( lastChangedItem ) + lastChangedItem->setCheckState( checkList.contains( lastChangedItem->data( Qt::UserRole ).toString() ) ? Qt::Checked : Qt::Unchecked ); + } else if ( mComboBox ) { @@ -202,7 +178,7 @@ void QgsValueRelationWidgetWrapper::setValue( const QVariant &value ) } else if ( mLineEdit ) { - Q_FOREACH ( QgsValueRelationFieldFormatter::ValueRelationItem i, mCache ) + for ( const QgsValueRelationFieldFormatter::ValueRelationItem &i : qgis::as_const( mCache ) ) { if ( i.key == value ) { @@ -213,19 +189,126 @@ void QgsValueRelationWidgetWrapper::setValue( const QVariant &value ) } } +void QgsValueRelationWidgetWrapper::widgetValueChanged( const QString &attribute, const QVariant &newValue, bool attributeChanged ) +{ + + // Do nothing if the value has not changed + if ( attributeChanged ) + { + mFeature.setAttribute( attribute, newValue ); + // Update combos if the value used in the filter expression has changed + if ( QgsValueRelationFieldFormatter::expressionRequiresFormScope( mExpression ) + && QgsValueRelationFieldFormatter::expressionFormAttributes( mExpression ).contains( attribute ) ) + { + populate(); + // Restore value + setValue( value( ) ); + } + } +} + + +void QgsValueRelationWidgetWrapper::setFeature( const QgsFeature &feature ) +{ + mFeature = feature; + whileBlocking( this )->populate(); + whileBlocking( this )->setValue( feature.attribute( mFieldIdx ) ); + // A bit of logic to set the default value if AllowNull is false and this is a new feature + // Note that this needs to be here after the cache has been created/updated by populate() + // and signals unblocked (we want this to propagate to the feature itself) + if ( mFeature.isValid() + && ! mFeature.attribute( mFieldIdx ).isValid() + && mCache.size() > 0 + && ! config( QStringLiteral( "AllowNull" ) ).toBool( ) ) + { + // This is deferred because at the time the feature is set in one widget it is not + // set in the next, which is typically the "down" in a drill-down + QTimer::singleShot( 0, [ = ] + { + setValue( mCache.at( 0 ).key ); + } ); + } +} + + +void QgsValueRelationWidgetWrapper::populate( ) +{ + // Initialize, note that signals are blocked, to avoid double signals on new features + if ( QgsValueRelationFieldFormatter::expressionRequiresFormScope( mExpression ) ) + { + mCache = QgsValueRelationFieldFormatter::createCache( config( ), mFeature ); + } + else if ( mCache.isEmpty() ) + { + mCache = QgsValueRelationFieldFormatter::createCache( config( ) ); + } + + if ( mComboBox ) + { + mComboBox->clear(); + if ( config( QStringLiteral( "AllowNull" ) ).toBool( ) ) + { + whileBlocking( mComboBox )->addItem( tr( "(no selection)" ), QVariant( field().type( ) ) ); + } + + for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : qgis::as_const( mCache ) ) + { + whileBlocking( mComboBox )->addItem( element.value, element.key ); + } + } + else if ( mTableWidget ) + { + if ( mCache.size() > 0 ) + mTableWidget->setRowCount( ( mCache.size() + config( QStringLiteral( "NofColumns" ) ).toInt() - 1 ) / config( QStringLiteral( "NofColumns" ) ).toInt() ); + else + mTableWidget->setRowCount( 1 ); + if ( config( QStringLiteral( "NofColumns" ) ).toInt() > 0 ) + mTableWidget->setColumnCount( config( QStringLiteral( "NofColumns" ) ).toInt() ); + else + mTableWidget->setColumnCount( 1 ); + + whileBlocking( mTableWidget )->clear(); + int row = 0, column = 0; + for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : qgis::as_const( mCache ) ) + { + if ( column == config( QStringLiteral( "NofColumns" ) ).toInt() ) + { + row++; + column = 0; + } + QTableWidgetItem *item = nullptr; + item = new QTableWidgetItem( element.value ); + item->setData( Qt::UserRole, element.key ); + whileBlocking( mTableWidget )->setItem( row, column, item ); + column++; + } + } + else if ( mLineEdit ) + { + QStringList values; + values.reserve( mCache.size() ); + for ( const QgsValueRelationFieldFormatter::ValueRelationItem &i : qgis::as_const( mCache ) ) + { + values << i.value; + } + QStringListModel *m = new QStringListModel( values, mLineEdit ); + QCompleter *completer = new QCompleter( m, mLineEdit ); + completer->setCaseSensitivity( Qt::CaseInsensitive ); + mLineEdit->setCompleter( completer ); + } +} + void QgsValueRelationWidgetWrapper::showIndeterminateState() { if ( mTableWidget ) { - mTableWidget->blockSignals( true ); for ( int j = 0; j < mTableWidget->rowCount(); j++ ) { for ( int i = 0; i < config( QStringLiteral( "NofColumns" ) ).toInt(); ++i ) { - mTableWidget->item( j, i )->setCheckState( Qt::PartiallyChecked ); + whileBlocking( mTableWidget )->item( j, i )->setCheckState( Qt::PartiallyChecked ); } } - mTableWidget->blockSignals( false ); } else if ( mComboBox ) { @@ -246,6 +329,7 @@ void QgsValueRelationWidgetWrapper::setEnabled( bool enabled ) if ( mTableWidget ) { + mTableWidget->blockSignals( true ); for ( int j = 0; j < mTableWidget->rowCount(); j++ ) { for ( int i = 0; i < mTableWidget->columnCount(); ++i ) @@ -260,6 +344,7 @@ void QgsValueRelationWidgetWrapper::setEnabled( bool enabled ) } } } + mTableWidget->blockSignals( false ); } else QgsEditorWidgetWrapper::setEnabled( enabled ); diff --git a/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.h b/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.h index 7dd5f2d62e4..591127d748c 100644 --- a/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.h +++ b/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.h @@ -68,9 +68,39 @@ class GUI_EXPORT QgsValueRelationWidgetWrapper : public QgsEditorWidgetWrapper bool valid() const override; public slots: + void setValue( const QVariant &value ) override; + /** + * Will be called when a value in the current edited form or table row + * changes + * + * Update widget cache if the value is used in the filter expression and + * stores current field values to be used in expression form scope context + * + * \param attribute The name of the attribute that changed. + * \param newValue The new value of the attribute. + * \param attributeChanged If true, it corresponds to an actual change of the feature attribute + * \since QGIS 3.2.0 + */ + void widgetValueChanged( const QString &attribute, const QVariant &newValue, bool attributeChanged ); + + /** + * Will be called when the feature changes + * + * Is forwarded to the slot setValue() and updates the widget cache if + * the filter expression context contains values from the current feature + * + * \param feature The new feature + */ + void setFeature( const QgsFeature &feature ) override; + + private: + + //! Set the values for the widgets, re-creates the cache when required + void populate( ); + QComboBox *mComboBox = nullptr; QTableWidget *mTableWidget = nullptr; QLineEdit *mLineEdit = nullptr; @@ -79,6 +109,7 @@ class GUI_EXPORT QgsValueRelationWidgetWrapper : public QgsEditorWidgetWrapper QgsVectorLayer *mLayer = nullptr; bool mEnabled = true; + QString mExpression; friend class QgsValueRelationWidgetFactory; friend class TestQgsValueRelationWidgetWrapper; diff --git a/src/gui/qgsattributeeditorcontext.h b/src/gui/qgsattributeeditorcontext.h index 8bbc7c5869c..5aa09acf646 100644 --- a/src/gui/qgsattributeeditorcontext.h +++ b/src/gui/qgsattributeeditorcontext.h @@ -63,6 +63,7 @@ class GUI_EXPORT QgsAttributeEditorContext , mVectorLayerTools( parentContext.mVectorLayerTools ) , mMapCanvas( parentContext.mMapCanvas ) , mDistanceArea( parentContext.mDistanceArea ) + , mFormFeature( parentContext.mFormFeature ) , mFormMode( formMode ) { Q_ASSERT( parentContext.vectorLayerTools() ); @@ -189,6 +190,21 @@ class GUI_EXPORT QgsAttributeEditorContext inline const QgsAttributeEditorContext *parentContext() const { return mParentContext; } + /** + * Return current feature from the currently edited form or table row + * \see setFormFeature() + * \since QGIS 3.2 + */ + const QgsFeature formFeature() const { return mFormFeature; } + + /** + * Set current \a feature for the currently edited form or table row + * \see formFeature() + * \since QGIS 3.2 + */ + void setFormFeature( const QgsFeature &feature ) { mFormFeature = feature ; } + + private: const QgsAttributeEditorContext *mParentContext = nullptr; QgsVectorLayer *mLayer = nullptr; @@ -197,6 +213,8 @@ class GUI_EXPORT QgsAttributeEditorContext QgsDistanceArea mDistanceArea; QgsRelation mRelation; RelationMode mRelationMode = Undefined; + //! Store the values of the currently edited form or table row + QgsFeature mFormFeature; FormMode mFormMode = Embed; bool mAllowCustomUi = true; }; diff --git a/src/gui/qgsattributeform.cpp b/src/gui/qgsattributeform.cpp index 43498a12008..14bd3083e3e 100644 --- a/src/gui/qgsattributeform.cpp +++ b/src/gui/qgsattributeform.cpp @@ -690,6 +690,7 @@ QString QgsAttributeForm::createFilterExpression() const return filter; } + void QgsAttributeForm::onAttributeChanged( const QVariant &value ) { QgsEditorWidgetWrapper *eww = qobject_cast( sender() ); diff --git a/src/gui/qgsattributeform.h b/src/gui/qgsattributeform.h index f0c7b87477c..96b7fcb7de9 100644 --- a/src/gui/qgsattributeform.h +++ b/src/gui/qgsattributeform.h @@ -174,7 +174,8 @@ class GUI_EXPORT QgsAttributeForm : public QWidget signals: /** - * Notifies about changes of attributes + * Notifies about changes of attributes, this signal is not emitted when the value is set + * back to the original one. * * \param attribute The name of the attribute that changed. * \param value The new value of the attribute. @@ -353,6 +354,8 @@ class GUI_EXPORT QgsAttributeForm : public QWidget QString createFilterExpression() const; + //void updateWidgetFeature( QgsEditorWidgetWrapper *w ); + //! constraints management void updateAllConstraints(); void updateConstraints( QgsEditorWidgetWrapper *w ); diff --git a/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp b/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp index aee5704e3b1..9fb9cef628e 100644 --- a/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp +++ b/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp @@ -3,7 +3,9 @@ -------------------------------------- Date : 21 07 2017 Copyright : (C) 2017 Paul Blottiere + Copyright : (C) 2018 Alessandro Pasotti Email : paul dot blottiere at oslandia dot com + Email : elpaso at itopen dot it *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * @@ -24,6 +26,7 @@ #include "qgseditorwidgetwrapper.h" #include #include +#include #include "qgsgui.h" class TestQgsValueRelationWidgetWrapper : public QObject @@ -39,6 +42,7 @@ class TestQgsValueRelationWidgetWrapper : public QObject void cleanup(); // will be called after every testfunction. void testScrollBarUnlocked(); + void testDrillDown(); }; void TestQgsValueRelationWidgetWrapper::initTestCase() @@ -110,5 +114,83 @@ void TestQgsValueRelationWidgetWrapper::testScrollBarUnlocked() QCOMPARE( itemEnabled, true ); } +void TestQgsValueRelationWidgetWrapper::testDrillDown() +{ + // create a vector layer + QgsVectorLayer vl1( QStringLiteral( "Polygon?crs=epsg:4326&field=pk:int&field=province:int&field=municipality:string" ), QStringLiteral( "vl1" ), QStringLiteral( "memory" ) ); + QgsVectorLayer vl2( QStringLiteral( "Point?crs=epsg:4326&field=pk:int&field=fk_province:int&field=fk_municipality:int" ), QStringLiteral( "vl2" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( &vl1, false, false ); + QgsProject::instance()->addMapLayer( &vl2, false, false ); + + // insert some features + QgsFeature f1( vl1.fields() ); + f1.setAttribute( QStringLiteral( "pk" ), 1 ); + f1.setAttribute( QStringLiteral( "province" ), 123 ); + f1.setAttribute( QStringLiteral( "municipality" ), QStringLiteral( "Some Place By The River" ) ); + f1.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POLYGON(( 0 0, 0 1, 1 1, 1 0, 0 0 ))" ) ) ); + QVERIFY( f1.isValid() ); + QgsFeature f2( vl1.fields() ); + f2.setAttribute( QStringLiteral( "pk" ), 2 ); + f2.setAttribute( QStringLiteral( "province" ), 245 ); + f2.setAttribute( QStringLiteral( "municipality" ), QStringLiteral( "Dreamland By The Clouds" ) ); + f2.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POLYGON(( 1 0, 1 1, 2 1, 2 0, 1 0 ))" ) ) ); + QVERIFY( f2.isValid() ); + QVERIFY( vl1.dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 ) ); + + QgsFeature f3( vl2.fields() ); + f3.setAttribute( QStringLiteral( "fk_province" ), 123 ); + f3.setAttribute( QStringLiteral( "fk_municipality" ), 1 ); + f3.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POINT( 0.5 0.5)" ) ) ); + QVERIFY( f3.isValid() ); + QVERIFY( f3.geometry().isGeosValid() ); + QVERIFY( vl2.dataProvider()->addFeature( f3 ) ); + + // build a value relation widget wrapper for municipality + QgsValueRelationWidgetWrapper w_municipality( &vl2, vl2.fields().indexOf( QStringLiteral( "fk_municipality" ) ), nullptr, nullptr ); + QVariantMap cfg_municipality; + cfg_municipality.insert( QStringLiteral( "Layer" ), vl1.id() ); + cfg_municipality.insert( QStringLiteral( "Key" ), QStringLiteral( "pk" ) ); + cfg_municipality.insert( QStringLiteral( "Value" ), QStringLiteral( "municipality" ) ); + cfg_municipality.insert( QStringLiteral( "AllowMulti" ), false ); + cfg_municipality.insert( QStringLiteral( "NofColumns" ), 1 ); + cfg_municipality.insert( QStringLiteral( "AllowNull" ), false ); + cfg_municipality.insert( QStringLiteral( "OrderByValue" ), true ); + cfg_municipality.insert( QStringLiteral( "FilterExpression" ), QStringLiteral( "\"province\" = get_current_form_field_value('fk_province')" ) ); + cfg_municipality.insert( QStringLiteral( "UseCompleter" ), false ); + w_municipality.setConfig( cfg_municipality ); + w_municipality.widget(); + w_municipality.setEnabled( true ); + + QCOMPARE( w_municipality.mCache.size(), 2 ); + QCOMPARE( w_municipality.mComboBox->count(), 2 ); + w_municipality.setFeature( f3 ); + QCOMPARE( w_municipality.mCache.size(), 1 ); + + QCOMPARE( w_municipality.mComboBox->count(), 1 ); + QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "Some Place By The River" ) ); + + // Filter by geometry + cfg_municipality[ QStringLiteral( "FilterExpression" ) ] = QStringLiteral( "contains(buffer(@current_form_geometry, 1 ), $geometry)" ); + w_municipality.setConfig( cfg_municipality ); + w_municipality.setFeature( f3 ); + QCOMPARE( w_municipality.mComboBox->count(), 1 ); + QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "Some Place By The River" ) ); + + // Move the point to 1.5 0.5 + f3.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POINT( 1.5 0.5)" ) ) ); + w_municipality.setFeature( f3 ); + QCOMPARE( w_municipality.mComboBox->count(), 1 ); + QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "Dreamland By The Clouds" ) ); + + // Enlarge the buffer + cfg_municipality[ QStringLiteral( "FilterExpression" ) ] = QStringLiteral( "contains(buffer(@current_form_geometry, 3 ), $geometry)" ); + w_municipality.setConfig( cfg_municipality ); + w_municipality.setFeature( f3 ); + QCOMPARE( w_municipality.mComboBox->count(), 2 ); + QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "Dreamland By The Clouds" ) ); + QCOMPARE( w_municipality.mComboBox->itemText( 1 ), QStringLiteral( "Some Place By The River" ) ); + +} + QGSTEST_MAIN( TestQgsValueRelationWidgetWrapper ) #include "testqgsvaluerelationwidgetwrapper.moc" diff --git a/tests/src/python/test_qgsfieldformatters.py b/tests/src/python/test_qgsfieldformatters.py index cb995ab7ce7..d12c3dd452a 100644 --- a/tests/src/python/test_qgsfieldformatters.py +++ b/tests/src/python/test_qgsfieldformatters.py @@ -16,7 +16,8 @@ import qgis # NOQA from qgis.core import (QgsFeature, QgsProject, QgsRelation, QgsVectorLayer, QgsValueMapFieldFormatter, QgsValueRelationFieldFormatter, - QgsRelationReferenceFieldFormatter, QgsRangeFieldFormatter, QgsSettings) + QgsRelationReferenceFieldFormatter, QgsRangeFieldFormatter, + QgsSettings, QgsGeometry, QgsPointXY) from qgis.PyQt.QtCore import QCoreApplication, QLocale from qgis.testing import start_app, unittest @@ -133,6 +134,39 @@ class TestQgsValueRelationFieldFormatter(unittest.TestCase): _test(['1', '2', '3'], ["1", "2", "3"]) _test('not an array', ['not an array']) + def test_expressionRequiresFormScope(self): + + res = list(QgsValueRelationFieldFormatter.expressionFormAttributes("get_current_form_field_value('ONE') AND get_current_form_field_value('TWO')")) + res = sorted(res) + self.assertEqual(res, ['ONE', 'TWO']) + + res = list(QgsValueRelationFieldFormatter.expressionFormVariables("current_form_geometry")) + self.assertEqual(res, ['current_form_geometry']) + + self.assertFalse(QgsValueRelationFieldFormatter.expressionRequiresFormScope("")) + self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("get_current_form_field_value('TWO')")) + self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("get_current_form_field_value ( 'TWO' )")) + self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("current_form_geometry")) + + self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("", QgsFeature())) + self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("current_form_geometry", QgsFeature())) + self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("get_current_form_field_value ( 'TWO' )", QgsFeature())) + + layer = QgsVectorLayer("none?field=pkid:integer&field=decoded:string", + "layer", "memory") + self.assertTrue(layer.isValid()) + QgsProject.instance().addMapLayer(layer) + f = QgsFeature(layer.fields()) + f.setAttributes([1, 'value']) + point = QgsGeometry.fromPointXY(QgsPointXY(123, 456)) + f.setGeometry(point) + self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("current_form_geometry", f)) + self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("get_current_form_field_value ( 'TWO' )", f)) + self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("get_current_form_field_value ( 'pkid' )", f)) + self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("@current_form_geometry get_current_form_field_value ( 'pkid' )", f)) + + QgsProject.instance().removeMapLayer(layer.id()) + class TestQgsRelationReferenceFieldFormatter(unittest.TestCase): From 63d2086de4d84e28daa233cf9c9224e178af8262 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Sun, 13 May 2018 11:27:36 +0200 Subject: [PATCH 02/13] Rename functions from get_current_form_field_value to current_value and current_geometry --- .../json/get_current_form_field_value.txt | 4 ++-- src/core/expression/qgsexpression.cpp | 2 +- src/core/qgsexpressioncontext.cpp | 6 ++--- .../gui/testqgsvaluerelationwidgetwrapper.cpp | 6 ++--- tests/src/python/test_qgsfieldformatters.py | 24 +++++++++---------- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/resources/function_help/json/get_current_form_field_value.txt b/resources/function_help/json/get_current_form_field_value.txt index 19b1c809e56..d04e2a33c0c 100644 --- a/resources/function_help/json/get_current_form_field_value.txt +++ b/resources/function_help/json/get_current_form_field_value.txt @@ -1,7 +1,7 @@ { - "name": "get_current_form_field_value", + "name": "current_value", "type": "function", "description": "Returns the current value of a field in the form or table row currently being edited.", "arguments": [ {"arg":"field_name","description":"a field name in the current form or table row"}], - "examples": [ { "expression":"get_current_form_field_value( 'FIELD_NAME' )","returns":"The current value of field 'FIELD_NAME'."} ] + "examples": [ { "expression":"current_value( 'FIELD_NAME' )","returns":"The current value of field 'FIELD_NAME'."} ] } diff --git a/src/core/expression/qgsexpression.cpp b/src/core/expression/qgsexpression.cpp index 85aec1a6757..ec06d9bebef 100644 --- a/src/core/expression/qgsexpression.cpp +++ b/src/core/expression/qgsexpression.cpp @@ -773,7 +773,7 @@ void QgsExpression::initVariableHelp() sVariableHelpTexts.insert( QStringLiteral( "notification_message" ), QCoreApplication::translate( "notification_message", "Content of the notification message sent by the provider (available only for actions triggered by provider notifications)." ) ); //form context variable - sVariableHelpTexts.insert( QStringLiteral( "current_form_geometry" ), QCoreApplication::translate( "current_form_geometry", "Represents the geometry of the feature currently being edited in the form or the table row. Can be used for in a form/row context to filter the related features." ) ); + sVariableHelpTexts.insert( QStringLiteral( "current_geometry" ), QCoreApplication::translate( "current_geometry", "Represents the geometry of the feature currently being edited in the form or the table row. Can be used for in a form/row context to filter the related features." ) ); } QString QgsExpression::variableHelpText( const QString &variableName ) diff --git a/src/core/qgsexpressioncontext.cpp b/src/core/qgsexpressioncontext.cpp index 0c780849770..d5787c8102b 100644 --- a/src/core/qgsexpressioncontext.cpp +++ b/src/core/qgsexpressioncontext.cpp @@ -741,7 +741,7 @@ class GetCurrentFormFieldValue : public QgsScopedExpressionFunction { public: GetCurrentFormFieldValue( ) - : QgsScopedExpressionFunction( QStringLiteral( "get_current_form_field_value" ), QgsExpressionFunction::ParameterList() << QStringLiteral( "field_name" ), QStringLiteral( "Form" ) ) + : QgsScopedExpressionFunction( QStringLiteral( "current_value" ), QgsExpressionFunction::ParameterList() << QStringLiteral( "field_name" ), QStringLiteral( "Form" ) ) {} QVariant func( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *, const QgsExpressionNodeFunction * ) override @@ -794,8 +794,8 @@ QgsExpressionContextScope *QgsExpressionContextUtils::formScope( const QgsFeatur { QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Form" ) ); scope->setFeature( formFeature ); - scope->addFunction( QStringLiteral( "get_current_form_field_value" ), new GetCurrentFormFieldValue( ) ); - scope->setVariable( QStringLiteral( "current_form_geometry" ), formFeature.geometry( ), true ); + scope->addFunction( QStringLiteral( "current_value" ), new GetCurrentFormFieldValue( ) ); + scope->setVariable( QStringLiteral( "current_geometry" ), formFeature.geometry( ), true ); return scope; } diff --git a/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp b/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp index 9fb9cef628e..8c1a686068b 100644 --- a/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp +++ b/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp @@ -155,7 +155,7 @@ void TestQgsValueRelationWidgetWrapper::testDrillDown() cfg_municipality.insert( QStringLiteral( "NofColumns" ), 1 ); cfg_municipality.insert( QStringLiteral( "AllowNull" ), false ); cfg_municipality.insert( QStringLiteral( "OrderByValue" ), true ); - cfg_municipality.insert( QStringLiteral( "FilterExpression" ), QStringLiteral( "\"province\" = get_current_form_field_value('fk_province')" ) ); + cfg_municipality.insert( QStringLiteral( "FilterExpression" ), QStringLiteral( "\"province\" = current_value('fk_province')" ) ); cfg_municipality.insert( QStringLiteral( "UseCompleter" ), false ); w_municipality.setConfig( cfg_municipality ); w_municipality.widget(); @@ -170,7 +170,7 @@ void TestQgsValueRelationWidgetWrapper::testDrillDown() QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "Some Place By The River" ) ); // Filter by geometry - cfg_municipality[ QStringLiteral( "FilterExpression" ) ] = QStringLiteral( "contains(buffer(@current_form_geometry, 1 ), $geometry)" ); + cfg_municipality[ QStringLiteral( "FilterExpression" ) ] = QStringLiteral( "contains(buffer(@current_geometry, 1 ), $geometry)" ); w_municipality.setConfig( cfg_municipality ); w_municipality.setFeature( f3 ); QCOMPARE( w_municipality.mComboBox->count(), 1 ); @@ -183,7 +183,7 @@ void TestQgsValueRelationWidgetWrapper::testDrillDown() QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "Dreamland By The Clouds" ) ); // Enlarge the buffer - cfg_municipality[ QStringLiteral( "FilterExpression" ) ] = QStringLiteral( "contains(buffer(@current_form_geometry, 3 ), $geometry)" ); + cfg_municipality[ QStringLiteral( "FilterExpression" ) ] = QStringLiteral( "contains(buffer(@current_geometry, 3 ), $geometry)" ); w_municipality.setConfig( cfg_municipality ); w_municipality.setFeature( f3 ); QCOMPARE( w_municipality.mComboBox->count(), 2 ); diff --git a/tests/src/python/test_qgsfieldformatters.py b/tests/src/python/test_qgsfieldformatters.py index d12c3dd452a..69a0564d993 100644 --- a/tests/src/python/test_qgsfieldformatters.py +++ b/tests/src/python/test_qgsfieldformatters.py @@ -136,21 +136,21 @@ class TestQgsValueRelationFieldFormatter(unittest.TestCase): def test_expressionRequiresFormScope(self): - res = list(QgsValueRelationFieldFormatter.expressionFormAttributes("get_current_form_field_value('ONE') AND get_current_form_field_value('TWO')")) + res = list(QgsValueRelationFieldFormatter.expressionFormAttributes("current_value('ONE') AND current_value('TWO')")) res = sorted(res) self.assertEqual(res, ['ONE', 'TWO']) - res = list(QgsValueRelationFieldFormatter.expressionFormVariables("current_form_geometry")) - self.assertEqual(res, ['current_form_geometry']) + res = list(QgsValueRelationFieldFormatter.expressionFormVariables("current_geometry")) + self.assertEqual(res, ['current_geometry']) self.assertFalse(QgsValueRelationFieldFormatter.expressionRequiresFormScope("")) - self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("get_current_form_field_value('TWO')")) - self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("get_current_form_field_value ( 'TWO' )")) - self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("current_form_geometry")) + self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("current_value('TWO')")) + self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("current_value ( 'TWO' )")) + self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("current_geometry")) self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("", QgsFeature())) - self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("current_form_geometry", QgsFeature())) - self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("get_current_form_field_value ( 'TWO' )", QgsFeature())) + self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("current_geometry", QgsFeature())) + self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("current_value ( 'TWO' )", QgsFeature())) layer = QgsVectorLayer("none?field=pkid:integer&field=decoded:string", "layer", "memory") @@ -160,10 +160,10 @@ class TestQgsValueRelationFieldFormatter(unittest.TestCase): f.setAttributes([1, 'value']) point = QgsGeometry.fromPointXY(QgsPointXY(123, 456)) f.setGeometry(point) - self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("current_form_geometry", f)) - self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("get_current_form_field_value ( 'TWO' )", f)) - self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("get_current_form_field_value ( 'pkid' )", f)) - self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("@current_form_geometry get_current_form_field_value ( 'pkid' )", f)) + self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("current_geometry", f)) + self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("current_value ( 'TWO' )", f)) + self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("current_value ( 'pkid' )", f)) + self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("@current_geometry current_value ( 'pkid' )", f)) QgsProject.instance().removeMapLayer(layer.id()) From ba339f2f1c013a50a169da05975e641c35e19b3e Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Mon, 14 May 2018 08:47:16 +0200 Subject: [PATCH 03/13] Some more test cases for value relation widget --- .../gui/testqgsvaluerelationwidgetwrapper.cpp | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp b/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp index 8c1a686068b..48530e62d57 100644 --- a/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp +++ b/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp @@ -43,6 +43,7 @@ class TestQgsValueRelationWidgetWrapper : public QObject void testScrollBarUnlocked(); void testDrillDown(); + void testDrillDownMulti(); }; void TestQgsValueRelationWidgetWrapper::initTestCase() @@ -63,6 +64,7 @@ void TestQgsValueRelationWidgetWrapper::init() void TestQgsValueRelationWidgetWrapper::cleanup() { + } void TestQgsValueRelationWidgetWrapper::testScrollBarUnlocked() @@ -166,8 +168,10 @@ void TestQgsValueRelationWidgetWrapper::testDrillDown() w_municipality.setFeature( f3 ); QCOMPARE( w_municipality.mCache.size(), 1 ); + // Check first is selected QCOMPARE( w_municipality.mComboBox->count(), 1 ); QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "Some Place By The River" ) ); + QCOMPARE( w_municipality.value().toString(), QStringLiteral( "1" ) ); // Filter by geometry cfg_municipality[ QStringLiteral( "FilterExpression" ) ] = QStringLiteral( "contains(buffer(@current_geometry, 1 ), $geometry)" ); @@ -190,6 +194,123 @@ void TestQgsValueRelationWidgetWrapper::testDrillDown() QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "Dreamland By The Clouds" ) ); QCOMPARE( w_municipality.mComboBox->itemText( 1 ), QStringLiteral( "Some Place By The River" ) ); + // Check with allow null + cfg_municipality[QStringLiteral( "AllowNull" )] = true; + w_municipality.setConfig( cfg_municipality ); + w_municipality.setFeature( QgsFeature() ); + + // Check null is selected + QCOMPARE( w_municipality.mComboBox->count(), 3 ); + QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "(no selection)" ) ); + QCOMPARE( w_municipality.value().toString(), QStringLiteral( "" ) ); + + // Check order by value false + cfg_municipality[QStringLiteral( "AllowNull" )] = false; + cfg_municipality[QStringLiteral( "OrderByValue" )] = false; + w_municipality.setConfig( cfg_municipality ); + w_municipality.setFeature( f3 ); + QCOMPARE( w_municipality.mComboBox->itemText( 1 ), QStringLiteral( "Dreamland By The Clouds" ) ); + QCOMPARE( w_municipality.mComboBox->itemText( 0 ), QStringLiteral( "Some Place By The River" ) ); + +} + + +void TestQgsValueRelationWidgetWrapper::testDrillDownMulti() +{ + // create a vector layer + QgsVectorLayer vl1( QStringLiteral( "Polygon?crs=epsg:4326&field=pk:int&field=province:int&field=municipality:string" ), QStringLiteral( "vl1" ), QStringLiteral( "memory" ) ); + QgsVectorLayer vl2( QStringLiteral( "Point?crs=epsg:4326&field=pk:int&field=fk_province:int&field=fk_municipality:int" ), QStringLiteral( "vl2" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( &vl1, false, false ); + QgsProject::instance()->addMapLayer( &vl2, false, false ); + + // insert some features + QgsFeature f1( vl1.fields() ); + f1.setAttribute( QStringLiteral( "pk" ), 1 ); + f1.setAttribute( QStringLiteral( "province" ), 123 ); + f1.setAttribute( QStringLiteral( "municipality" ), QStringLiteral( "Some Place By The River" ) ); + f1.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POLYGON(( 0 0, 0 1, 1 1, 1 0, 0 0 ))" ) ) ); + QVERIFY( f1.isValid() ); + QgsFeature f2( vl1.fields() ); + f2.setAttribute( QStringLiteral( "pk" ), 2 ); + f2.setAttribute( QStringLiteral( "province" ), 245 ); + f2.setAttribute( QStringLiteral( "municipality" ), QStringLiteral( "Dreamland By The Clouds" ) ); + f2.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POLYGON(( 1 0, 1 1, 2 1, 2 0, 1 0 ))" ) ) ); + QVERIFY( f2.isValid() ); + QVERIFY( vl1.dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 ) ); + + QgsFeature f3( vl2.fields() ); + f3.setAttribute( QStringLiteral( "fk_province" ), 123 ); + f3.setAttribute( QStringLiteral( "fk_municipality" ), QStringLiteral( "{1}" ) ); + f3.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POINT( 0.5 0.5)" ) ) ); + QVERIFY( f3.isValid() ); + QVERIFY( f3.geometry().isGeosValid() ); + QVERIFY( vl2.dataProvider()->addFeature( f3 ) ); + + // build a value relation widget wrapper for municipality + QgsValueRelationWidgetWrapper w_municipality( &vl2, vl2.fields().indexOf( QStringLiteral( "fk_municipality" ) ), nullptr, nullptr ); + QVariantMap cfg_municipality; + cfg_municipality.insert( QStringLiteral( "Layer" ), vl1.id() ); + cfg_municipality.insert( QStringLiteral( "Key" ), QStringLiteral( "pk" ) ); + cfg_municipality.insert( QStringLiteral( "Value" ), QStringLiteral( "municipality" ) ); + cfg_municipality.insert( QStringLiteral( "AllowMulti" ), true ); + cfg_municipality.insert( QStringLiteral( "NofColumns" ), 1 ); + cfg_municipality.insert( QStringLiteral( "AllowNull" ), false ); + cfg_municipality.insert( QStringLiteral( "OrderByValue" ), true ); + cfg_municipality.insert( QStringLiteral( "FilterExpression" ), QStringLiteral( "\"province\" = current_value('fk_province')" ) ); + cfg_municipality.insert( QStringLiteral( "UseCompleter" ), false ); + w_municipality.setConfig( cfg_municipality ); + w_municipality.widget(); + w_municipality.setEnabled( true ); + + QCOMPARE( w_municipality.mCache.size(), 2 ); + QCOMPARE( w_municipality.mTableWidget->rowCount(), 2 ); + w_municipality.setFeature( f3 ); + QCOMPARE( w_municipality.mCache.size(), 1 ); + + QCOMPARE( w_municipality.mTableWidget->rowCount(), 1 ); + QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->text(), QStringLiteral( "Some Place By The River" ) ); + QCOMPARE( w_municipality.value().toString(), QStringLiteral( "{1}" ) ); + + // Filter by geometry + cfg_municipality[ QStringLiteral( "FilterExpression" ) ] = QStringLiteral( "contains(buffer(@current_geometry, 1 ), $geometry)" ); + w_municipality.setConfig( cfg_municipality ); + w_municipality.setFeature( f3 ); + QCOMPARE( w_municipality.mTableWidget->rowCount(), 1 ); + QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->text(), QStringLiteral( "Some Place By The River" ) ); + + // Move the point to 1.5 0.5 + f3.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "POINT( 1.5 0.5)" ) ) ); + w_municipality.setFeature( f3 ); + QCOMPARE( w_municipality.mTableWidget->rowCount(), 1 ); + QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->text(), QStringLiteral( "Dreamland By The Clouds" ) ); + + // Enlarge the buffer + cfg_municipality[ QStringLiteral( "FilterExpression" ) ] = QStringLiteral( "contains(buffer(@current_geometry, 3 ), $geometry)" ); + w_municipality.setConfig( cfg_municipality ); + w_municipality.setFeature( f3 ); + QCOMPARE( w_municipality.mTableWidget->rowCount(), 2 ); + QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->text(), QStringLiteral( "Dreamland By The Clouds" ) ); + QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->data( Qt::UserRole ).toString(), QStringLiteral( "2" ) ); + QCOMPARE( w_municipality.mTableWidget->item( 1, 0 )->text(), QStringLiteral( "Some Place By The River" ) ); + QCOMPARE( w_municipality.mTableWidget->item( 1, 0 )->data( Qt::UserRole ).toString(), QStringLiteral( "1" ) ); + QCOMPARE( w_municipality.value().toString(), QStringLiteral( "{1}" ) ); + QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->checkState(), Qt::Unchecked ); + QCOMPARE( w_municipality.mTableWidget->item( 1, 0 )->checkState(), Qt::Checked ); + w_municipality.setValue( QStringLiteral( "{1,2}" ) ); + QCOMPARE( w_municipality.value().toString(), QStringLiteral( "{2,1}" ) ); + QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->checkState(), Qt::Checked ); + QCOMPARE( w_municipality.mTableWidget->item( 1, 0 )->checkState(), Qt::Checked ); + + // Check values are checked + f3.setAttribute( QStringLiteral( "fk_municipality" ), QStringLiteral( "{1,2}" ) ); + w_municipality.setFeature( f3 ); + QCOMPARE( w_municipality.mTableWidget->rowCount(), 2 ); + QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->text(), QStringLiteral( "Dreamland By The Clouds" ) ); + QCOMPARE( w_municipality.mTableWidget->item( 1, 0 )->text(), QStringLiteral( "Some Place By The River" ) ); + QCOMPARE( w_municipality.mTableWidget->item( 0, 0 )->checkState(), Qt::Checked ); + QCOMPARE( w_municipality.mTableWidget->item( 1, 0 )->checkState(), Qt::Checked ); + QCOMPARE( w_municipality.value().toString(), QStringLiteral( "{2,1}" ) ); + } QGSTEST_MAIN( TestQgsValueRelationWidgetWrapper ) From e59d6fee22b62d8980075d86c2419a1a71c5c27b Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Mon, 14 May 2018 10:16:37 +0200 Subject: [PATCH 04/13] Removed leftover --- src/gui/qgsattributeform.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/gui/qgsattributeform.h b/src/gui/qgsattributeform.h index 96b7fcb7de9..3cece194633 100644 --- a/src/gui/qgsattributeform.h +++ b/src/gui/qgsattributeform.h @@ -354,8 +354,6 @@ class GUI_EXPORT QgsAttributeForm : public QWidget QString createFilterExpression() const; - //void updateWidgetFeature( QgsEditorWidgetWrapper *w ); - //! constraints management void updateAllConstraints(); void updateConstraints( QgsEditorWidgetWrapper *w ); From 266f145c99219e0741069d668b6917bc2fd3f279 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Mon, 14 May 2018 10:17:58 +0200 Subject: [PATCH 05/13] Removed const I don't get why, but I trust @m-kuhn and I'll follow his advice :) --- src/gui/qgsattributeeditorcontext.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/qgsattributeeditorcontext.h b/src/gui/qgsattributeeditorcontext.h index 5aa09acf646..b1c2e667100 100644 --- a/src/gui/qgsattributeeditorcontext.h +++ b/src/gui/qgsattributeeditorcontext.h @@ -195,7 +195,7 @@ class GUI_EXPORT QgsAttributeEditorContext * \see setFormFeature() * \since QGIS 3.2 */ - const QgsFeature formFeature() const { return mFormFeature; } + QgsFeature formFeature() const { return mFormFeature; } /** * Set current \a feature for the currently edited form or table row From 541737681b791186243f1ac72b6edd5c79d74672 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Mon, 14 May 2018 10:19:47 +0200 Subject: [PATCH 06/13] Moved form feature to private and added setters/getters plus other minor style changes as suggested by m-kuhn in his PR review --- .../qgsvaluerelationfieldformatter.sip.in | 2 +- .../core/qgseditorwidgetwrapper.sip.in | 27 +++++++++++++- .../qgsattributeeditorcontext.sip.in | 2 +- .../qgsvaluerelationfieldformatter.cpp | 6 ++-- .../qgsvaluerelationfieldformatter.h | 2 +- .../core/qgseditorwidgetwrapper.cpp | 9 +++-- .../core/qgseditorwidgetwrapper.h | 35 ++++++++++++++++--- .../qgsvaluerelationwidgetwrapper.cpp | 14 ++++---- 8 files changed, 76 insertions(+), 21 deletions(-) diff --git a/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in b/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in index f8e91857944..3a617f93d8f 100644 --- a/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in +++ b/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in @@ -118,7 +118,7 @@ Return a list of variables required by the form context ``expression`` static bool expressionIsUsable( const QString &expression, const QgsFeature &feature ); %Docstring -Check wether the ``feature`` has all values required by the ``expression`` +Check whether the ``feature`` has all values required by the ``expression`` @return True if the expression can be used diff --git a/python/gui/auto_generated/editorwidgets/core/qgseditorwidgetwrapper.sip.in b/python/gui/auto_generated/editorwidgets/core/qgseditorwidgetwrapper.sip.in index 355ce4ee247..bd09ef7e5a9 100644 --- a/python/gui/auto_generated/editorwidgets/core/qgseditorwidgetwrapper.sip.in +++ b/python/gui/auto_generated/editorwidgets/core/qgseditorwidgetwrapper.sip.in @@ -213,6 +213,7 @@ This will be disabled when the form is not editable. .. versionadded:: 3.0 %End + signals: void valueChanged( const QVariant &value ); @@ -279,8 +280,32 @@ change the visual cue. .. versionadded:: 2.16 %End - protected: + QgsFeature formFeature() const; +%Docstring +The feature currently being edited, in its current state + +:return: the feature currently being edited, in its current state + +.. versionadded:: 3.2 +%End + + void setFormFeature( const QgsFeature &feature ); +%Docstring +Set the feature currently being edited to ``feature`` + +.. versionadded:: 3.2 +%End + + bool setFormFeatureAttribute( const QString &attributeName, const QVariant &attributeValue ); +%Docstring +Update the feature currently being edited by changing its +attribute ``attributeName`` to ``attributeValue`` + +:return: bool true on success + +.. versionadded:: 3.2 +%End }; diff --git a/python/gui/auto_generated/qgsattributeeditorcontext.sip.in b/python/gui/auto_generated/qgsattributeeditorcontext.sip.in index 759952b6e3f..ef9d370b83c 100644 --- a/python/gui/auto_generated/qgsattributeeditorcontext.sip.in +++ b/python/gui/auto_generated/qgsattributeeditorcontext.sip.in @@ -180,7 +180,7 @@ QGIS forms const QgsAttributeEditorContext *parentContext() const; - const QgsFeature formFeature() const; + QgsFeature formFeature() const; %Docstring Return current feature from the currently edited form or table row diff --git a/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp b/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp index 2e7e8a0a526..652b659a78f 100644 --- a/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp +++ b/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp @@ -179,11 +179,11 @@ QSet QgsValueRelationFieldFormatter::expressionFormVariables( const QSt const QStringList formVariables( QgsExpressionContextUtils::formScope()->variableNames() ); QSet variables; - for ( auto it = formVariables.constBegin(); it != formVariables.constEnd(); it++ ) + for ( auto const &variable : formVariables ) { - if ( expression.contains( *it ) ) + if ( expression.contains( variable ) ) { - variables.insert( *it ); + variables.insert( variable ); } } return variables; diff --git a/src/core/fieldformatter/qgsvaluerelationfieldformatter.h b/src/core/fieldformatter/qgsvaluerelationfieldformatter.h index 3ba24b11e80..083cbd96a3f 100644 --- a/src/core/fieldformatter/qgsvaluerelationfieldformatter.h +++ b/src/core/fieldformatter/qgsvaluerelationfieldformatter.h @@ -115,7 +115,7 @@ class CORE_EXPORT QgsValueRelationFieldFormatter : public QgsFieldFormatter static QSet expressionFormVariables( const QString &expression ); /** - * Check wether the \a feature has all values required by the \a expression + * Check whether the \a feature has all values required by the \a expression * * @return True if the expression can be used * \since QGIS 3.2 diff --git a/src/gui/editorwidgets/core/qgseditorwidgetwrapper.cpp b/src/gui/editorwidgets/core/qgseditorwidgetwrapper.cpp index 097bf8a16e3..2110a0ea761 100644 --- a/src/gui/editorwidgets/core/qgseditorwidgetwrapper.cpp +++ b/src/gui/editorwidgets/core/qgseditorwidgetwrapper.cpp @@ -25,9 +25,9 @@ QgsEditorWidgetWrapper::QgsEditorWidgetWrapper( QgsVectorLayer *vl, int fieldIdx, QWidget *editor, QWidget *parent ) : QgsWidgetWrapper( vl, editor, parent ) + , mFieldIdx( fieldIdx ) , mValidConstraint( true ) , mIsBlockingCommit( false ) - , mFieldIdx( fieldIdx ) { } @@ -68,7 +68,7 @@ void QgsEditorWidgetWrapper::setEnabled( bool enabled ) void QgsEditorWidgetWrapper::setFeature( const QgsFeature &feature ) { - mFeature = feature; + mFormFeature = feature; setValue( feature.attribute( mFieldIdx ) ); } @@ -102,6 +102,11 @@ void QgsEditorWidgetWrapper::updateConstraintWidgetStatus() } } +bool QgsEditorWidgetWrapper::setFormFeatureAttribute( const QString &attributeName, const QVariant &attributeValue ) +{ + return mFormFeature.setAttribute( attributeName, attributeValue ); +} + QgsEditorWidgetWrapper::ConstraintResult QgsEditorWidgetWrapper::constraintResult() const { return mConstraintResult; diff --git a/src/gui/editorwidgets/core/qgseditorwidgetwrapper.h b/src/gui/editorwidgets/core/qgseditorwidgetwrapper.h index 588d4dab265..47cd2b744f9 100644 --- a/src/gui/editorwidgets/core/qgseditorwidgetwrapper.h +++ b/src/gui/editorwidgets/core/qgseditorwidgetwrapper.h @@ -213,6 +213,7 @@ class GUI_EXPORT QgsEditorWidgetWrapper : public QgsWidgetWrapper */ void setConstraintResultVisible( bool constraintResultVisible ); + signals: /** @@ -277,7 +278,33 @@ class GUI_EXPORT QgsEditorWidgetWrapper : public QgsWidgetWrapper */ virtual void updateConstraintWidgetStatus(); - protected: + + /** + * The feature currently being edited, in its current state + * + * \return the feature currently being edited, in its current state + * \since QGIS 3.2 + */ + QgsFeature formFeature() const { return mFormFeature; } + + /** + * Set the feature currently being edited to \a feature + * + * \since QGIS 3.2 + */ + void setFormFeature( const QgsFeature &feature ) { mFormFeature = feature; } + + /** + * Update the feature currently being edited by changing its + * attribute \a attributeName to \a attributeValue + * + * \return bool true on success + * \since QGIS 3.2 + */ + bool setFormFeatureAttribute( const QString &attributeName, const QVariant &attributeValue ); + + + private: /** * mFieldIdx the widget feature field id @@ -285,11 +312,9 @@ class GUI_EXPORT QgsEditorWidgetWrapper : public QgsWidgetWrapper int mFieldIdx; /** - * mFeature the current feature + * The feature currently being edited, in its current state */ - QgsFeature mFeature; - - private: + QgsFeature mFormFeature; /** * Boolean storing the current validity of the constraint for this widget. diff --git a/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp b/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp index e7eec52f859..6a3d4d67a7a 100644 --- a/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp @@ -87,7 +87,7 @@ QVariant QgsValueRelationWidgetWrapper::value() const QWidget *QgsValueRelationWidgetWrapper::createWidget( QWidget *parent ) { - QgsAttributeForm *form = dynamic_cast( parent ); + QgsAttributeForm *form = qobject_cast( parent ); if ( form ) connect( form, &QgsAttributeForm::widgetValueChanged, this, &QgsValueRelationWidgetWrapper::widgetValueChanged ); @@ -195,7 +195,7 @@ void QgsValueRelationWidgetWrapper::widgetValueChanged( const QString &attribute // Do nothing if the value has not changed if ( attributeChanged ) { - mFeature.setAttribute( attribute, newValue ); + setFormFeatureAttribute( attribute, newValue ); // Update combos if the value used in the filter expression has changed if ( QgsValueRelationFieldFormatter::expressionRequiresFormScope( mExpression ) && QgsValueRelationFieldFormatter::expressionFormAttributes( mExpression ).contains( attribute ) ) @@ -210,14 +210,14 @@ void QgsValueRelationWidgetWrapper::widgetValueChanged( const QString &attribute void QgsValueRelationWidgetWrapper::setFeature( const QgsFeature &feature ) { - mFeature = feature; + setFormFeature( feature ); whileBlocking( this )->populate(); - whileBlocking( this )->setValue( feature.attribute( mFieldIdx ) ); + whileBlocking( this )->setValue( feature.attribute( fieldIdx() ) ); // A bit of logic to set the default value if AllowNull is false and this is a new feature // Note that this needs to be here after the cache has been created/updated by populate() // and signals unblocked (we want this to propagate to the feature itself) - if ( mFeature.isValid() - && ! mFeature.attribute( mFieldIdx ).isValid() + if ( formFeature().isValid() + && ! formFeature().attribute( fieldIdx() ).isValid() && mCache.size() > 0 && ! config( QStringLiteral( "AllowNull" ) ).toBool( ) ) { @@ -236,7 +236,7 @@ void QgsValueRelationWidgetWrapper::populate( ) // Initialize, note that signals are blocked, to avoid double signals on new features if ( QgsValueRelationFieldFormatter::expressionRequiresFormScope( mExpression ) ) { - mCache = QgsValueRelationFieldFormatter::createCache( config( ), mFeature ); + mCache = QgsValueRelationFieldFormatter::createCache( config( ), formFeature() ); } else if ( mCache.isEmpty() ) { From edb12b4dbed5efe5e9730a0c83ac8ac2409f5098 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Tue, 15 May 2018 08:34:39 +0200 Subject: [PATCH 07/13] Removed attribution --- tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp b/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp index 48530e62d57..178b5b6bb68 100644 --- a/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp +++ b/tests/src/gui/testqgsvaluerelationwidgetwrapper.cpp @@ -3,9 +3,7 @@ -------------------------------------- Date : 21 07 2017 Copyright : (C) 2017 Paul Blottiere - Copyright : (C) 2018 Alessandro Pasotti Email : paul dot blottiere at oslandia dot com - Email : elpaso at itopen dot it *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * From b4f86147c79f0c04c2ee006a1ef00f4fbc51b772 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Tue, 15 May 2018 09:20:10 +0200 Subject: [PATCH 08/13] Add referencedFunctions() to expressions --- .../expression/qgsexpression.sip.in | 7 +++ .../expression/qgsexpressionnode.sip.in | 5 ++ .../expression/qgsexpressionnodeimpl.sip.in | 15 +++++ src/core/expression/qgsexpression.cpp | 8 +++ src/core/expression/qgsexpression.h | 7 +++ src/core/expression/qgsexpressionnode.cpp | 1 + src/core/expression/qgsexpressionnode.h | 5 ++ src/core/expression/qgsexpressionnodeimpl.cpp | 62 +++++++++++++++++++ src/core/expression/qgsexpressionnodeimpl.h | 8 +++ tests/src/core/testqgsexpression.cpp | 18 ++++++ 10 files changed, 136 insertions(+) diff --git a/python/core/auto_generated/expression/qgsexpression.sip.in b/python/core/auto_generated/expression/qgsexpression.sip.in index 7a079ce64ae..d11a5da4afb 100644 --- a/python/core/auto_generated/expression/qgsexpression.sip.in +++ b/python/core/auto_generated/expression/qgsexpression.sip.in @@ -176,6 +176,13 @@ If the list contains a NULL QString, there is a variable name used which is determined at runtime. .. versionadded:: 3.0 +%End + + QSet referencedFunctions() const; +%Docstring +Return a list of all functions which are used in this expression. + +.. versionadded:: 3.2 %End QSet referencedAttributeIndexes( const QgsFields &fields ) const; diff --git a/python/core/auto_generated/expression/qgsexpressionnode.sip.in b/python/core/auto_generated/expression/qgsexpressionnode.sip.in index d3b4d147ada..8910a617fbb 100644 --- a/python/core/auto_generated/expression/qgsexpressionnode.sip.in +++ b/python/core/auto_generated/expression/qgsexpressionnode.sip.in @@ -196,6 +196,11 @@ to evaluate child nodes. virtual QSet referencedVariables() const = 0; %Docstring Return a set of all variables which are used in this expression. +%End + + virtual QSet referencedFunctions() const = 0; +%Docstring +Return a set of all functions which are used in this expression. %End virtual bool needsGeometry() const = 0; diff --git a/python/core/auto_generated/expression/qgsexpressionnodeimpl.sip.in b/python/core/auto_generated/expression/qgsexpressionnodeimpl.sip.in index 6337b4dfd09..97925ee6786 100644 --- a/python/core/auto_generated/expression/qgsexpressionnodeimpl.sip.in +++ b/python/core/auto_generated/expression/qgsexpressionnodeimpl.sip.in @@ -55,6 +55,8 @@ Returns the node the operator will operate upon. virtual QSet referencedVariables() const; + virtual QSet referencedFunctions() const; + virtual bool needsGeometry() const; virtual QgsExpressionNode *clone() const /Factory/; @@ -154,6 +156,9 @@ Returns the node to the right of the operator. virtual QSet referencedVariables() const; + virtual QSet referencedFunctions() const; + + virtual bool needsGeometry() const; virtual QgsExpressionNode *clone() const /Factory/; @@ -224,6 +229,8 @@ Returns the list of nodes to search for matching values within. virtual QSet referencedVariables() const; + virtual QSet referencedFunctions() const; + virtual bool needsGeometry() const; virtual QgsExpressionNode *clone() const /Factory/; @@ -275,6 +282,8 @@ Returns a list of arguments specified for the function. virtual QSet referencedVariables() const; + virtual QSet referencedFunctions() const; + virtual bool needsGeometry() const; virtual QgsExpressionNode *clone() const /Factory/; @@ -323,6 +332,8 @@ The value of the literal. virtual QSet referencedVariables() const; + virtual QSet referencedFunctions() const; + virtual bool needsGeometry() const; virtual QgsExpressionNode *clone() const /Factory/; @@ -367,6 +378,8 @@ The name of the column. virtual QSet referencedVariables() const; + virtual QSet referencedFunctions() const; + virtual bool needsGeometry() const; @@ -466,6 +479,8 @@ The ELSE expression used for the condition. virtual QSet referencedVariables() const; + virtual QSet referencedFunctions() const; + virtual bool needsGeometry() const; virtual QgsExpressionNode *clone() const /Factory/; diff --git a/src/core/expression/qgsexpression.cpp b/src/core/expression/qgsexpression.cpp index ec06d9bebef..d76d3b4d422 100644 --- a/src/core/expression/qgsexpression.cpp +++ b/src/core/expression/qgsexpression.cpp @@ -276,6 +276,14 @@ QSet QgsExpression::referencedVariables() const return d->mRootNode->referencedVariables(); } +QSet QgsExpression::referencedFunctions() const +{ + if ( !d->mRootNode ) + return QSet(); + + return d->mRootNode->referencedFunctions(); +} + QSet QgsExpression::referencedAttributeIndexes( const QgsFields &fields ) const { if ( !d->mRootNode ) diff --git a/src/core/expression/qgsexpression.h b/src/core/expression/qgsexpression.h index c5c34149283..6f84fbb617e 100644 --- a/src/core/expression/qgsexpression.h +++ b/src/core/expression/qgsexpression.h @@ -258,6 +258,13 @@ class CORE_EXPORT QgsExpression */ QSet referencedVariables() const; + /** + * Return a list of all functions which are used in this expression. + * + * \since QGIS 3.2 + */ + QSet referencedFunctions() const; + /** * Return a list of field name indexes obtained from the provided fields. * diff --git a/src/core/expression/qgsexpressionnode.cpp b/src/core/expression/qgsexpressionnode.cpp index eca888315b7..ad00274236f 100644 --- a/src/core/expression/qgsexpressionnode.cpp +++ b/src/core/expression/qgsexpressionnode.cpp @@ -57,3 +57,4 @@ void QgsExpressionNode::cloneTo( QgsExpressionNode *target ) const target->parserFirstColumn = parserFirstColumn; target->parserFirstLine = parserFirstLine; } + diff --git a/src/core/expression/qgsexpressionnode.h b/src/core/expression/qgsexpressionnode.h index adc77ab7024..5cc0faf5fc7 100644 --- a/src/core/expression/qgsexpressionnode.h +++ b/src/core/expression/qgsexpressionnode.h @@ -222,6 +222,11 @@ class CORE_EXPORT QgsExpressionNode SIP_ABSTRACT */ virtual QSet referencedVariables() const = 0; + /** + * Return a set of all functions which are used in this expression. + */ + virtual QSet referencedFunctions() const = 0; + /** * Abstract virtual method which returns if the geometry is required to evaluate * this expression. diff --git a/src/core/expression/qgsexpressionnodeimpl.cpp b/src/core/expression/qgsexpressionnodeimpl.cpp index f4acc14083d..dc7cb175868 100644 --- a/src/core/expression/qgsexpressionnodeimpl.cpp +++ b/src/core/expression/qgsexpressionnodeimpl.cpp @@ -137,6 +137,11 @@ QSet QgsExpressionNodeUnaryOperator::referencedVariables() const return mOperand->referencedVariables(); } +QSet QgsExpressionNodeUnaryOperator::referencedFunctions() const +{ + return mOperand->referencedFunctions(); +} + bool QgsExpressionNodeUnaryOperator::needsGeometry() const { return mOperand->needsGeometry(); @@ -720,6 +725,11 @@ QSet QgsExpressionNodeBinaryOperator::referencedVariables() const return mOpLeft->referencedVariables() + mOpRight->referencedVariables(); } +QSet QgsExpressionNodeBinaryOperator::referencedFunctions() const +{ + return mOpLeft->referencedFunctions() + mOpRight->referencedFunctions(); +} + bool QgsExpressionNodeBinaryOperator::needsGeometry() const { return mOpLeft->needsGeometry() || mOpRight->needsGeometry(); @@ -978,6 +988,23 @@ QSet QgsExpressionNodeFunction::referencedVariables() const } } +QSet QgsExpressionNodeFunction::referencedFunctions() const +{ + QgsExpressionFunction *fd = QgsExpression::QgsExpression::Functions()[mFnIndex]; + QSet functions = QSet(); + functions.insert( fd->name() ); + + if ( !mArgs ) + return functions; + + const QList< QgsExpressionNode * > nodeList = mArgs->list(); + for ( QgsExpressionNode *n : nodeList ) + { + functions.unite( n->referencedFunctions() ); + } + return functions; +} + bool QgsExpressionNodeFunction::needsGeometry() const { bool needs = QgsExpression::QgsExpression::Functions()[mFnIndex]->usesGeometry( this ); @@ -1128,6 +1155,11 @@ QSet QgsExpressionNodeLiteral::referencedVariables() const return QSet(); } +QSet QgsExpressionNodeLiteral::referencedFunctions() const +{ + return QSet(); +} + bool QgsExpressionNodeLiteral::needsGeometry() const { return false; @@ -1215,6 +1247,11 @@ QSet QgsExpressionNodeColumnRef::referencedVariables() const return QSet(); } +QSet QgsExpressionNodeColumnRef::referencedFunctions() const +{ + return QSet(); +} + bool QgsExpressionNodeColumnRef::needsGeometry() const { return false; @@ -1338,6 +1375,20 @@ QSet QgsExpressionNodeCondition::referencedVariables() const return lst; } +QSet QgsExpressionNodeCondition::referencedFunctions() const +{ + QSet lst; + for ( WhenThen *cond : mConditions ) + { + lst += cond->mWhenExp->referencedFunctions() + cond->mThenExp->referencedFunctions(); + } + + if ( mElseExp ) + lst += mElseExp->referencedFunctions(); + + return lst; +} + bool QgsExpressionNodeCondition::needsGeometry() const { for ( WhenThen *cond : mConditions ) @@ -1393,6 +1444,16 @@ QSet QgsExpressionNodeInOperator::referencedVariables() const return lst; } +QSet QgsExpressionNodeInOperator::referencedFunctions() const +{ + QSet lst( mNode->referencedFunctions() ); + const QList< QgsExpressionNode * > nodeList = mList->list(); + for ( const QgsExpressionNode *n : nodeList ) + lst.unite( n->referencedFunctions() ); + return lst; + +} + QgsExpressionNodeCondition::WhenThen::WhenThen( QgsExpressionNode *whenExp, QgsExpressionNode *thenExp ) : mWhenExp( whenExp ) , mThenExp( thenExp ) @@ -1414,3 +1475,4 @@ QString QgsExpressionNodeBinaryOperator::text() const { return BINARY_OPERATOR_TEXT[mOp]; } + diff --git a/src/core/expression/qgsexpressionnodeimpl.h b/src/core/expression/qgsexpressionnodeimpl.h index 908f65f7ffa..fbf155b73d6 100644 --- a/src/core/expression/qgsexpressionnodeimpl.h +++ b/src/core/expression/qgsexpressionnodeimpl.h @@ -64,6 +64,7 @@ class CORE_EXPORT QgsExpressionNodeUnaryOperator : public QgsExpressionNode QSet referencedColumns() const override; QSet referencedVariables() const override; + QSet referencedFunctions() const override; bool needsGeometry() const override; QgsExpressionNode *clone() const override SIP_FACTORY; @@ -162,6 +163,8 @@ class CORE_EXPORT QgsExpressionNodeBinaryOperator : public QgsExpressionNode QSet referencedColumns() const override; QSet referencedVariables() const override; + QSet referencedFunctions() const override; + bool needsGeometry() const override; QgsExpressionNode *clone() const override SIP_FACTORY; bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override; @@ -241,6 +244,7 @@ class CORE_EXPORT QgsExpressionNodeInOperator : public QgsExpressionNode QSet referencedColumns() const override; QSet referencedVariables() const override; + QSet referencedFunctions() const override; bool needsGeometry() const override; QgsExpressionNode *clone() const override SIP_FACTORY; bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override; @@ -284,6 +288,7 @@ class CORE_EXPORT QgsExpressionNodeFunction : public QgsExpressionNode QSet referencedColumns() const override; QSet referencedVariables() const override; + QSet referencedFunctions() const override; bool needsGeometry() const override; QgsExpressionNode *clone() const override SIP_FACTORY; bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override; @@ -321,6 +326,7 @@ class CORE_EXPORT QgsExpressionNodeLiteral : public QgsExpressionNode QSet referencedColumns() const override; QSet referencedVariables() const override; + QSet referencedFunctions() const override; bool needsGeometry() const override; QgsExpressionNode *clone() const override SIP_FACTORY; bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override; @@ -356,6 +362,7 @@ class CORE_EXPORT QgsExpressionNodeColumnRef : public QgsExpressionNode QSet referencedColumns() const override; QSet referencedVariables() const override; + QSet referencedFunctions() const override; bool needsGeometry() const override; QgsExpressionNode *clone() const override SIP_FACTORY; @@ -456,6 +463,7 @@ class CORE_EXPORT QgsExpressionNodeCondition : public QgsExpressionNode QSet referencedColumns() const override; QSet referencedVariables() const override; + QSet referencedFunctions() const override; bool needsGeometry() const override; QgsExpressionNode *clone() const override SIP_FACTORY; bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override; diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index c031e6387d1..65ff5cb5aea 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -1861,6 +1861,24 @@ class TestQgsExpression: public QObject QCOMPARE( refVar, expectedVars ); } + + void referenced_functions() + { + QSet expectedFunctions; + expectedFunctions << QStringLiteral( "current_value" ) + << QStringLiteral( "var" ) + << QStringLiteral( "intersects" ) + << QStringLiteral( "$geometry" ) + << QStringLiteral( "buffer" ); + + QgsExpression exp( QStringLiteral( "current_value( 'FIELD_NAME' ) = \"A_VALUE\" AND intersects(buffer($geometry, 10), @current_geometry)" ) ); + QCOMPARE( exp.hasParserError(), false ); + QSet refVar = exp.referencedFunctions(); + + QCOMPARE( refVar, expectedFunctions ); + } + + void referenced_columns_all_attributes() { QgsExpression exp( QStringLiteral( "attribute($currentfeature,'test')" ) ); From 1e046a1726b30788e3787b86e1a424b397bc523f Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Tue, 15 May 2018 12:45:43 +0200 Subject: [PATCH 09/13] Updated docs for current_value function --- .../json/{get_current_form_field_value.txt => current_value} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename resources/function_help/json/{get_current_form_field_value.txt => current_value} (51%) diff --git a/resources/function_help/json/get_current_form_field_value.txt b/resources/function_help/json/current_value similarity index 51% rename from resources/function_help/json/get_current_form_field_value.txt rename to resources/function_help/json/current_value index d04e2a33c0c..2810a1a2259 100644 --- a/resources/function_help/json/get_current_form_field_value.txt +++ b/resources/function_help/json/current_value @@ -1,7 +1,7 @@ { "name": "current_value", "type": "function", - "description": "Returns the current value of a field in the form or table row currently being edited.", + "description": "Returns the current, unsaved value of a field in the form or table row currently being edited. This will differ from the feature's actual attribute values for features which are currently being edited or have not yet been added to a layer.", "arguments": [ {"arg":"field_name","description":"a field name in the current form or table row"}], "examples": [ { "expression":"current_value( 'FIELD_NAME' )","returns":"The current value of field 'FIELD_NAME'."} ] } From 673fac0c888d54f4e384173ca308fe2972e9d5d7 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Tue, 15 May 2018 18:32:40 +0200 Subject: [PATCH 10/13] Added methods to search expression nodes and drop RegExp - nodes() return the list of all nodes - findNodes returns a list of the nodes matching the class Also drops the regexp for finding form attrs in the value-relation expressions. Other minor fixes as suggested in the PR review. --- .../expression/qgsexpression.sip.in | 3 +- .../expression/qgsexpressionnodeimpl.sip.in | 8 +-- .../qgsvaluerelationfieldformatter.sip.in | 10 +-- src/core/expression/qgsexpression.cpp | 11 +++ src/core/expression/qgsexpression.h | 35 +++++++++- src/core/expression/qgsexpressionnode.h | 8 +++ src/core/expression/qgsexpressionnodeimpl.cpp | 69 +++++++++++++++++++ src/core/expression/qgsexpressionnodeimpl.h | 12 ++++ .../qgsvaluerelationfieldformatter.cpp | 41 +++++------ .../qgsvaluerelationfieldformatter.h | 15 +--- tests/src/core/testqgsexpression.cpp | 37 +++++++++- tests/src/python/test_qgsfieldformatters.py | 6 +- 12 files changed, 198 insertions(+), 57 deletions(-) diff --git a/python/core/auto_generated/expression/qgsexpression.sip.in b/python/core/auto_generated/expression/qgsexpression.sip.in index d11a5da4afb..24d8cd3dbea 100644 --- a/python/core/auto_generated/expression/qgsexpression.sip.in +++ b/python/core/auto_generated/expression/qgsexpression.sip.in @@ -180,11 +180,12 @@ which is determined at runtime. QSet referencedFunctions() const; %Docstring -Return a list of all functions which are used in this expression. +Return a list of the names of all functions which are used in this expression. .. versionadded:: 3.2 %End + QSet referencedAttributeIndexes( const QgsFields &fields ) const; %Docstring Return a list of field name indexes obtained from the provided fields. diff --git a/python/core/auto_generated/expression/qgsexpressionnodeimpl.sip.in b/python/core/auto_generated/expression/qgsexpressionnodeimpl.sip.in index 97925ee6786..84325c2c2d6 100644 --- a/python/core/auto_generated/expression/qgsexpressionnodeimpl.sip.in +++ b/python/core/auto_generated/expression/qgsexpressionnodeimpl.sip.in @@ -57,8 +57,6 @@ Returns the node the operator will operate upon. virtual QSet referencedFunctions() const; - virtual bool needsGeometry() const; - virtual QgsExpressionNode *clone() const /Factory/; @@ -158,7 +156,6 @@ Returns the node to the right of the operator. virtual QSet referencedFunctions() const; - virtual bool needsGeometry() const; virtual QgsExpressionNode *clone() const /Factory/; @@ -231,8 +228,6 @@ Returns the list of nodes to search for matching values within. virtual QSet referencedFunctions() const; - virtual bool needsGeometry() const; - virtual QgsExpressionNode *clone() const /Factory/; virtual bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const; @@ -284,7 +279,6 @@ Returns a list of arguments specified for the function. virtual QSet referencedFunctions() const; - virtual bool needsGeometry() const; virtual QgsExpressionNode *clone() const /Factory/; @@ -334,7 +328,6 @@ The value of the literal. virtual QSet referencedFunctions() const; - virtual bool needsGeometry() const; virtual QgsExpressionNode *clone() const /Factory/; @@ -481,6 +474,7 @@ The ELSE expression used for the condition. virtual QSet referencedFunctions() const; + virtual bool needsGeometry() const; virtual QgsExpressionNode *clone() const /Factory/; diff --git a/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in b/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in index 3a617f93d8f..0b7e5a04c57 100644 --- a/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in +++ b/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in @@ -59,11 +59,7 @@ Constructor for QgsValueRelationFieldFormatter. static QStringList valueToStringList( const QVariant &value ); %Docstring -Utility to convert an array or a string representation of and array ``value`` to a string list - -:param value: The value to be converted - -:return: A string list +Utility to convert an array or a string representation of an array ``value`` to a string list .. versionadded:: 3.2 %End @@ -120,13 +116,11 @@ Return a list of variables required by the form context ``expression`` %Docstring Check whether the ``feature`` has all values required by the ``expression`` -@return True if the expression can be used +:return: True if the expression can be used .. versionadded:: 3.2 %End - static QString FORM_SCOPE_FUNCTIONS_RE; - }; diff --git a/src/core/expression/qgsexpression.cpp b/src/core/expression/qgsexpression.cpp index d76d3b4d422..411db0f87f3 100644 --- a/src/core/expression/qgsexpression.cpp +++ b/src/core/expression/qgsexpression.cpp @@ -956,3 +956,14 @@ bool QgsExpression::isField() const { return d->mRootNode && d->mRootNode->nodeType() == QgsExpressionNode::ntColumnRef; } + +QList QgsExpression::nodes() const +{ + if ( !d->mRootNode ) + return QList(); + + return d->mRootNode->nodes(); +} + + + diff --git a/src/core/expression/qgsexpression.h b/src/core/expression/qgsexpression.h index 6f84fbb617e..717799536bb 100644 --- a/src/core/expression/qgsexpression.h +++ b/src/core/expression/qgsexpression.h @@ -29,6 +29,7 @@ #include "qgis.h" #include "qgsunittypes.h" #include "qgsinterval.h" +#include "qgsexpressionnode.h" class QgsFeature; class QgsGeometry; @@ -41,7 +42,6 @@ class QgsDistanceArea; class QDomElement; class QgsExpressionContext; class QgsExpressionPrivate; -class QgsExpressionNode; class QgsExpressionFunction; /** @@ -259,12 +259,43 @@ class CORE_EXPORT QgsExpression QSet referencedVariables() const; /** - * Return a list of all functions which are used in this expression. + * Return a list of the names of all functions which are used in this expression. * * \since QGIS 3.2 */ QSet referencedFunctions() const; +#ifndef SIP_RUN + + /** + * Return a list of all nodes which are used in this expression + * + * \note not available in Python bindings + * \since QGIS 3.2 + */ + QList nodes( ) const; + + /** + * Return a list of all nodes of the given class which are used in this expression + * + * \note not available in Python bindings + * \since QGIS 3.2 + */ + template + QList findNodes( ) const + { + QList lst; + const QList allNodes( nodes() ); + for ( const auto &node : allNodes ) + { + const T *n = dynamic_cast( node ); + if ( n ) + lst << n; + } + return lst; + } +#endif + /** * Return a list of field name indexes obtained from the provided fields. * diff --git a/src/core/expression/qgsexpressionnode.h b/src/core/expression/qgsexpressionnode.h index 5cc0faf5fc7..c6970d4e0bf 100644 --- a/src/core/expression/qgsexpressionnode.h +++ b/src/core/expression/qgsexpressionnode.h @@ -227,6 +227,14 @@ class CORE_EXPORT QgsExpressionNode SIP_ABSTRACT */ virtual QSet referencedFunctions() const = 0; + /** + * Return a list of all nodes which are used in this expression. + * + * \note not available in Python bindings + * \since QGIS 3.2 + */ + virtual QList nodes( ) const = 0; SIP_SKIP + /** * Abstract virtual method which returns if the geometry is required to evaluate * this expression. diff --git a/src/core/expression/qgsexpressionnodeimpl.cpp b/src/core/expression/qgsexpressionnodeimpl.cpp index dc7cb175868..231a491b518 100644 --- a/src/core/expression/qgsexpressionnodeimpl.cpp +++ b/src/core/expression/qgsexpressionnodeimpl.cpp @@ -142,6 +142,14 @@ QSet QgsExpressionNodeUnaryOperator::referencedFunctions() const return mOperand->referencedFunctions(); } +QList QgsExpressionNodeUnaryOperator::nodes() const +{ + QList lst; + lst.append( this ); + lst += mOperand->nodes(); + return lst; +} + bool QgsExpressionNodeUnaryOperator::needsGeometry() const { return mOperand->needsGeometry(); @@ -730,6 +738,14 @@ QSet QgsExpressionNodeBinaryOperator::referencedFunctions() const return mOpLeft->referencedFunctions() + mOpRight->referencedFunctions(); } +QList QgsExpressionNodeBinaryOperator::nodes() const +{ + QList lst; + lst << this; + lst += mOpLeft->nodes() + mOpRight->nodes(); + return lst; +} + bool QgsExpressionNodeBinaryOperator::needsGeometry() const { return mOpLeft->needsGeometry() || mOpRight->needsGeometry(); @@ -1005,6 +1021,21 @@ QSet QgsExpressionNodeFunction::referencedFunctions() const return functions; } +QList QgsExpressionNodeFunction::nodes() const +{ + QList lst; + lst << this; + if ( !mArgs ) + return lst; + + const QList< QgsExpressionNode * > nodeList = mArgs->list(); + for ( QgsExpressionNode *n : nodeList ) + { + lst += n->nodes(); + } + return lst; +} + bool QgsExpressionNodeFunction::needsGeometry() const { bool needs = QgsExpression::QgsExpression::Functions()[mFnIndex]->usesGeometry( this ); @@ -1160,6 +1191,13 @@ QSet QgsExpressionNodeLiteral::referencedFunctions() const return QSet(); } +QList QgsExpressionNodeLiteral::nodes() const +{ + QList lst; + lst << this; + return lst; +} + bool QgsExpressionNodeLiteral::needsGeometry() const { return false; @@ -1252,6 +1290,13 @@ QSet QgsExpressionNodeColumnRef::referencedFunctions() const return QSet(); } +QList QgsExpressionNodeColumnRef::nodes() const +{ + QList result; + result << this; + return result; +} + bool QgsExpressionNodeColumnRef::needsGeometry() const { return false; @@ -1389,6 +1434,21 @@ QSet QgsExpressionNodeCondition::referencedFunctions() const return lst; } +QList QgsExpressionNodeCondition::nodes() const +{ + QList lst; + lst << this; + for ( WhenThen *cond : mConditions ) + { + lst += cond->mWhenExp->nodes() + cond->mThenExp->nodes(); + } + + if ( mElseExp ) + lst += mElseExp->nodes(); + + return lst; +} + bool QgsExpressionNodeCondition::needsGeometry() const { for ( WhenThen *cond : mConditions ) @@ -1451,7 +1511,16 @@ QSet QgsExpressionNodeInOperator::referencedFunctions() const for ( const QgsExpressionNode *n : nodeList ) lst.unite( n->referencedFunctions() ); return lst; +} +QList QgsExpressionNodeInOperator::nodes() const +{ + QList lst; + lst << this; + const QList< QgsExpressionNode * > nodeList = mList->list(); + for ( const QgsExpressionNode *n : nodeList ) + lst += n->nodes(); + return lst; } QgsExpressionNodeCondition::WhenThen::WhenThen( QgsExpressionNode *whenExp, QgsExpressionNode *thenExp ) diff --git a/src/core/expression/qgsexpressionnodeimpl.h b/src/core/expression/qgsexpressionnodeimpl.h index fbf155b73d6..d3ac023d63c 100644 --- a/src/core/expression/qgsexpressionnodeimpl.h +++ b/src/core/expression/qgsexpressionnodeimpl.h @@ -65,6 +65,7 @@ class CORE_EXPORT QgsExpressionNodeUnaryOperator : public QgsExpressionNode QSet referencedColumns() const override; QSet referencedVariables() const override; QSet referencedFunctions() const override; + QList nodes() const override; SIP_SKIP bool needsGeometry() const override; QgsExpressionNode *clone() const override SIP_FACTORY; @@ -164,6 +165,7 @@ class CORE_EXPORT QgsExpressionNodeBinaryOperator : public QgsExpressionNode QSet referencedColumns() const override; QSet referencedVariables() const override; QSet referencedFunctions() const override; + QList nodes( ) const override; SIP_SKIP bool needsGeometry() const override; QgsExpressionNode *clone() const override SIP_FACTORY; @@ -245,6 +247,7 @@ class CORE_EXPORT QgsExpressionNodeInOperator : public QgsExpressionNode QSet referencedColumns() const override; QSet referencedVariables() const override; QSet referencedFunctions() const override; + QList nodes() const override; SIP_SKIP bool needsGeometry() const override; QgsExpressionNode *clone() const override SIP_FACTORY; bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override; @@ -289,6 +292,8 @@ class CORE_EXPORT QgsExpressionNodeFunction : public QgsExpressionNode QSet referencedColumns() const override; QSet referencedVariables() const override; QSet referencedFunctions() const override; + + QList nodes() const override; SIP_SKIP bool needsGeometry() const override; QgsExpressionNode *clone() const override SIP_FACTORY; bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override; @@ -327,6 +332,8 @@ class CORE_EXPORT QgsExpressionNodeLiteral : public QgsExpressionNode QSet referencedColumns() const override; QSet referencedVariables() const override; QSet referencedFunctions() const override; + + QList nodes() const override; SIP_SKIP bool needsGeometry() const override; QgsExpressionNode *clone() const override SIP_FACTORY; bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override; @@ -363,6 +370,8 @@ class CORE_EXPORT QgsExpressionNodeColumnRef : public QgsExpressionNode QSet referencedColumns() const override; QSet referencedVariables() const override; QSet referencedFunctions() const override; + QList nodes( ) const override; SIP_SKIP + bool needsGeometry() const override; QgsExpressionNode *clone() const override SIP_FACTORY; @@ -464,6 +473,9 @@ class CORE_EXPORT QgsExpressionNodeCondition : public QgsExpressionNode QSet referencedColumns() const override; QSet referencedVariables() const override; QSet referencedFunctions() const override; + + QList nodes() const override; SIP_SKIP + bool needsGeometry() const override; QgsExpressionNode *clone() const override SIP_FACTORY; bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override; diff --git a/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp b/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp index 652b659a78f..773bd1ca315 100644 --- a/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp +++ b/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp @@ -18,11 +18,10 @@ #include "qgis.h" #include "qgsproject.h" #include "qgsvectorlayer.h" +#include "qgsexpressionnodeimpl.h" #include -QString QgsValueRelationFieldFormatter::FORM_SCOPE_FUNCTIONS_RE = QStringLiteral( "%1\\s*\\(\\s*'([^']+)'\\s*\\)" ); - bool orderByKeyLessThan( const QgsValueRelationFieldFormatter::ValueRelationItem &p1, const QgsValueRelationFieldFormatter::ValueRelationItem &p2 ) { return qgsVariantLessThan( p1.key, p2.key ); @@ -176,17 +175,11 @@ QStringList QgsValueRelationFieldFormatter::valueToStringList( const QVariant &v QSet QgsValueRelationFieldFormatter::expressionFormVariables( const QString &expression ) { - const QStringList formVariables( QgsExpressionContextUtils::formScope()->variableNames() ); - QSet variables; - - for ( auto const &variable : formVariables ) - { - if ( expression.contains( variable ) ) - { - variables.insert( variable ); - } - } - return variables; + std::unique_ptr< QgsExpressionContextScope > scope( QgsExpressionContextUtils::formScope() ); + QSet< QString > formVariables = scope->variableNames().toSet(); + const QSet< QString > usedVariables = QgsExpression( expression ).referencedVariables(); + formVariables.intersect( usedVariables ); + return formVariables; } bool QgsValueRelationFieldFormatter::expressionRequiresFormScope( const QString &expression ) @@ -197,18 +190,22 @@ bool QgsValueRelationFieldFormatter::expressionRequiresFormScope( const QString QSet QgsValueRelationFieldFormatter::expressionFormAttributes( const QString &expression ) { QSet attributes; - const QStringList formFunctions( QgsExpressionContextUtils::formScope()->functionNames() ); - QRegularExpression re; - for ( const auto &fname : formFunctions ) + QgsExpression exp( expression ); + std::unique_ptr< QgsExpressionContextScope > scope( QgsExpressionContextUtils::formScope() ); + // List of form function names used in the expression + const QSet formFunctions( scope->functionNames() + .toSet() + .intersect( exp.referencedFunctions( ) ) ); + const QList expFunctions( exp.findNodes() ); + const QgsExpressionContext context; + for ( const auto &f : expFunctions ) { - if ( QgsExpressionContextUtils::formScope()->function( fname )->parameters().count( ) != 0 ) + QgsExpressionFunction *fd = QgsExpression::QgsExpression::Functions()[f->fnIndex()]; + if ( formFunctions.contains( fd->name( ) ) ) { - re.setPattern( QgsValueRelationFieldFormatter::FORM_SCOPE_FUNCTIONS_RE.arg( fname ) ); - QRegularExpressionMatchIterator i = re.globalMatch( expression ); - while ( i.hasNext() ) + for ( const auto ¶m : f->args( )->list() ) { - QRegularExpressionMatch match = i.next(); - attributes.insert( match.captured( 1 ) ); + attributes.insert( param->eval( &exp, &context ).toString() ); } } } diff --git a/src/core/fieldformatter/qgsvaluerelationfieldformatter.h b/src/core/fieldformatter/qgsvaluerelationfieldformatter.h index 083cbd96a3f..9fabdea0faa 100644 --- a/src/core/fieldformatter/qgsvaluerelationfieldformatter.h +++ b/src/core/fieldformatter/qgsvaluerelationfieldformatter.h @@ -66,10 +66,7 @@ class CORE_EXPORT QgsValueRelationFieldFormatter : public QgsFieldFormatter QVariant createCache( QgsVectorLayer *layer, int fieldIndex, const QVariantMap &config ) const override; /** - * Utility to convert an array or a string representation of and array \a value to a string list - * - * \param value The value to be converted - * \return A string list + * Utility to convert an array or a string representation of an array \a value to a string list * \since QGIS 3.2 */ static QStringList valueToStringList( const QVariant &value ); @@ -117,19 +114,11 @@ class CORE_EXPORT QgsValueRelationFieldFormatter : public QgsFieldFormatter /** * Check whether the \a feature has all values required by the \a expression * - * @return True if the expression can be used + * \return True if the expression can be used * \since QGIS 3.2 */ static bool expressionIsUsable( const QString &expression, const QgsFeature &feature ); - /** - * Regular expression to find dynamic filtering based on form field values - * \see GetCurrentFormFieldValue() - * - * \since QGIS 3.2 - */ - static QString FORM_SCOPE_FUNCTIONS_RE; - }; Q_DECLARE_METATYPE( QgsValueRelationFieldFormatter::ValueRelationCache ) diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index 65ff5cb5aea..b0eedc705a3 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -1871,13 +1871,48 @@ class TestQgsExpression: public QObject << QStringLiteral( "$geometry" ) << QStringLiteral( "buffer" ); - QgsExpression exp( QStringLiteral( "current_value( 'FIELD_NAME' ) = \"A_VALUE\" AND intersects(buffer($geometry, 10), @current_geometry)" ) ); + QgsExpression exp( QStringLiteral( "current_value( 'FIELD_NAME' ) = 'A_VALUE' AND intersects(buffer($geometry, 10), @current_geometry)" ) ); QCOMPARE( exp.hasParserError(), false ); QSet refVar = exp.referencedFunctions(); QCOMPARE( refVar, expectedFunctions ); } + void findNodes() + { + QSet expectedFunctions; + expectedFunctions << QStringLiteral( "current_value" ) + << QStringLiteral( "intersects" ) + << QStringLiteral( "var" ) + << QStringLiteral( "$geometry" ) + << QStringLiteral( "buffer" ); + QgsExpression exp( QStringLiteral( "current_value( 'FIELD_NAME' ) = 'A_VALUE' AND intersects(buffer($geometry, 10), @current_geometry)" ) ); + QList functionNodes( exp.findNodes() ); + QCOMPARE( functionNodes.size(), 5 ); + QgsExpressionFunction *fd; + QSet actualFunctions; + for ( const auto &f : functionNodes ) + { + QCOMPARE( f->nodeType(), QgsExpressionNode::NodeType::ntFunction ); + fd = QgsExpression::QgsExpression::Functions()[f->fnIndex()]; + actualFunctions << fd->name(); + } + QCOMPARE( actualFunctions, expectedFunctions ); + + QSet expectedBinaryOps; + expectedBinaryOps << QgsExpressionNodeBinaryOperator::BinaryOperator::boAnd; + expectedBinaryOps << QgsExpressionNodeBinaryOperator::BinaryOperator::boEQ; + QList binaryOpsNodes( exp.findNodes() ); + QCOMPARE( binaryOpsNodes.size(), 2 ); + QSet actualBinaryOps; + for ( const auto &f : binaryOpsNodes ) + { + QCOMPARE( f->nodeType(), QgsExpressionNode::NodeType::ntBinaryOperator ); + actualBinaryOps << f->op(); + } + QCOMPARE( actualBinaryOps, expectedBinaryOps ); + + } void referenced_columns_all_attributes() { diff --git a/tests/src/python/test_qgsfieldformatters.py b/tests/src/python/test_qgsfieldformatters.py index 69a0564d993..6e7778cba5a 100644 --- a/tests/src/python/test_qgsfieldformatters.py +++ b/tests/src/python/test_qgsfieldformatters.py @@ -140,16 +140,16 @@ class TestQgsValueRelationFieldFormatter(unittest.TestCase): res = sorted(res) self.assertEqual(res, ['ONE', 'TWO']) - res = list(QgsValueRelationFieldFormatter.expressionFormVariables("current_geometry")) + res = list(QgsValueRelationFieldFormatter.expressionFormVariables("@current_geometry")) self.assertEqual(res, ['current_geometry']) self.assertFalse(QgsValueRelationFieldFormatter.expressionRequiresFormScope("")) self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("current_value('TWO')")) self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("current_value ( 'TWO' )")) - self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("current_geometry")) + self.assertTrue(QgsValueRelationFieldFormatter.expressionRequiresFormScope("@current_geometry")) self.assertTrue(QgsValueRelationFieldFormatter.expressionIsUsable("", QgsFeature())) - self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("current_geometry", QgsFeature())) + self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("@current_geometry", QgsFeature())) self.assertFalse(QgsValueRelationFieldFormatter.expressionIsUsable("current_value ( 'TWO' )", QgsFeature())) layer = QgsVectorLayer("none?field=pkid:integer&field=decoded:string", From 55a1984b28fcebe50ee3fd8b019486d2b80a8cbc Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Tue, 15 May 2018 18:36:57 +0200 Subject: [PATCH 11/13] Initialize mFieldIdx to -1 --- src/gui/editorwidgets/core/qgseditorwidgetwrapper.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/editorwidgets/core/qgseditorwidgetwrapper.h b/src/gui/editorwidgets/core/qgseditorwidgetwrapper.h index 47cd2b744f9..8dae88990cf 100644 --- a/src/gui/editorwidgets/core/qgseditorwidgetwrapper.h +++ b/src/gui/editorwidgets/core/qgseditorwidgetwrapper.h @@ -309,7 +309,7 @@ class GUI_EXPORT QgsEditorWidgetWrapper : public QgsWidgetWrapper /** * mFieldIdx the widget feature field id */ - int mFieldIdx; + int mFieldIdx = -1; /** * The feature currently being edited, in its current state From 4033cc2ebd411767adacfc61946c6294aa7a8519 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Tue, 15 May 2018 19:17:32 +0200 Subject: [PATCH 12/13] Set form feature in current_feature variable instead of ... setting the feature member variable --- src/core/qgsexpressioncontext.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/qgsexpressioncontext.cpp b/src/core/qgsexpressioncontext.cpp index d5787c8102b..37fca724a09 100644 --- a/src/core/qgsexpressioncontext.cpp +++ b/src/core/qgsexpressioncontext.cpp @@ -747,7 +747,7 @@ class GetCurrentFormFieldValue : public QgsScopedExpressionFunction QVariant func( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *, const QgsExpressionNodeFunction * ) override { QString fieldName( values.at( 0 ).toString() ); - const QgsFeature feat( context->feature() ); + const QgsFeature feat( context->variable( QStringLiteral( "current_feature" ) ).value() ); if ( fieldName.isEmpty() || ! feat.isValid( ) ) { return QVariant(); @@ -793,9 +793,9 @@ class GetProcessingParameterValue : public QgsScopedExpressionFunction QgsExpressionContextScope *QgsExpressionContextUtils::formScope( const QgsFeature &formFeature ) { QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Form" ) ); - scope->setFeature( formFeature ); scope->addFunction( QStringLiteral( "current_value" ), new GetCurrentFormFieldValue( ) ); scope->setVariable( QStringLiteral( "current_geometry" ), formFeature.geometry( ), true ); + scope->setVariable( QStringLiteral( "current_feature" ), formFeature, true ); return scope; } From b2fb029c263b189e089f2aeae0cdc3c44d9eddf0 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Wed, 16 May 2018 08:20:10 +0200 Subject: [PATCH 13/13] Added help text for @current_feature --- src/core/expression/qgsexpression.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/expression/qgsexpression.cpp b/src/core/expression/qgsexpression.cpp index 411db0f87f3..cef0e8c6759 100644 --- a/src/core/expression/qgsexpression.cpp +++ b/src/core/expression/qgsexpression.cpp @@ -782,6 +782,7 @@ void QgsExpression::initVariableHelp() //form context variable sVariableHelpTexts.insert( QStringLiteral( "current_geometry" ), QCoreApplication::translate( "current_geometry", "Represents the geometry of the feature currently being edited in the form or the table row. Can be used for in a form/row context to filter the related features." ) ); + sVariableHelpTexts.insert( QStringLiteral( "current_feature" ), QCoreApplication::translate( "current_feature", "Represents the feature currently being edited in the form or the table row. Can be used for in a form/row context to filter the related features." ) ); } QString QgsExpression::variableHelpText( const QString &variableName )