Skip to content

Commit 83328ae

Browse files
committed
[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
1 parent 4d36f37 commit 83328ae

23 files changed

+607
-83
lines changed

python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99

1010

11+
1112
class QgsValueRelationFieldFormatter : QgsFieldFormatter
1213
{
1314
%Docstring
@@ -56,25 +57,76 @@ Constructor for QgsValueRelationFieldFormatter.
5657
virtual QVariant createCache( QgsVectorLayer *layer, int fieldIndex, const QVariantMap &config ) const;
5758

5859

59-
static QgsValueRelationFieldFormatter::ValueRelationCache createCache( const QVariantMap &config );
60+
static QStringList valueToStringList( const QVariant &value );
61+
%Docstring
62+
Utility to convert an array or a string representation of and array ``value`` to a string list
63+
64+
:param value: The value to be converted
65+
66+
:return: A string list
67+
68+
.. versionadded:: 3.2
69+
%End
70+
71+
static QgsValueRelationFieldFormatter::ValueRelationCache createCache( const QVariantMap &config, const QgsFeature &formFeature = QgsFeature() );
6072
%Docstring
6173
Create a cache for a value relation field.
6274
This can be used to keep the value map in the local memory
6375
if doing multiple lookups in a loop.
6476

77+
:param config: The widget configuration
78+
:param formFeature: The feature currently being edited with current attribute values
79+
80+
:return: A kvp list of values for the widget
81+
6582
.. versionadded:: 3.0
6683
%End
6784

68-
static QStringList valueToStringList( const QVariant &value );
85+
static bool expressionRequiresFormScope( const QString &expression );
6986
%Docstring
70-
Utility to convert an array or a string representation of and array ``value`` to a string list
87+
Check if the ``expression`` requires a form scope (i.e. if it uses fields
88+
or geometry of the currently edited feature).
7189

72-
:param value: The value to be converted
90+
:param expression: The widget's filter expression
7391

74-
:return: A string list
92+
:return: true if the expression requires a form scope
93+
94+
.. versionadded:: 3.2
95+
%End
96+
97+
static QSet<QString> expressionFormAttributes( const QString &expression );
98+
%Docstring
99+
Return a list of attributes required by the form context ``expression``
100+
101+
:param expression: Form filter expression
102+
103+
:return: list of attributes required by the expression
104+
105+
.. versionadded:: 3.2
106+
%End
107+
108+
static QSet<QString> expressionFormVariables( const QString &expression );
109+
%Docstring
110+
Return a list of variables required by the form context ``expression``
111+
112+
:param expression: Form filter expression
113+
114+
:return: list of variables required by the expression
115+
116+
.. versionadded:: 3.2
117+
%End
118+
119+
static bool expressionIsUsable( const QString &expression, const QgsFeature &feature );
120+
%Docstring
121+
Check wether the ``feature`` has all values required by the ``expression``
122+
123+
@return True if the expression can be used
75124

76125
.. versionadded:: 3.2
77126
%End
127+
128+
static QString FORM_SCOPE_FUNCTIONS_RE;
129+
78130
};
79131

80132

python/core/auto_generated/qgsexpressioncontext.sip.in

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,14 @@ Creates a new scope which contains variables and functions relating to the globa
763763
For instance, QGIS version numbers and variables specified through QGIS options.
764764

765765
.. seealso:: :py:func:`setGlobalVariable`
766+
%End
767+
768+
static QgsExpressionContextScope *formScope( const QgsFeature &formFeature = QgsFeature( ) ) /Factory/;
769+
%Docstring
770+
Creates a new scope which contains functions and variables from the current attribute form/table feature.
771+
The variables and values in this scope will reflect the current state of the form/row being edited.
772+
773+
.. versionadded:: 3.2
766774
%End
767775

768776
static void setGlobalVariable( const QString &name, const QVariant &value );

python/gui/auto_generated/editorwidgets/core/qgseditorwidgetwrapper.sip.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,10 @@ change the visual cue.
279279
.. versionadded:: 2.16
280280
%End
281281

282+
protected:
283+
284+
285+
282286
};
283287

284288

python/gui/auto_generated/qgsattributeeditorcontext.sip.in

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,25 @@ QGIS forms
180180

181181
const QgsAttributeEditorContext *parentContext() const;
182182

183+
const QgsFeature formFeature() const;
184+
%Docstring
185+
Return current feature from the currently edited form or table row
186+
187+
.. seealso:: :py:func:`setFormFeature`
188+
189+
.. versionadded:: 3.2
190+
%End
191+
192+
void setFormFeature( const QgsFeature &feature );
193+
%Docstring
194+
Set current ``feature`` for the currently edited form or table row
195+
196+
.. seealso:: :py:func:`formFeature`
197+
198+
.. versionadded:: 3.2
199+
%End
200+
201+
183202
};
184203

185204
/************************************************************************

python/gui/auto_generated/qgsattributeform.sip.in

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ on all attribute widgets.
141141

142142
void attributeChanged( const QString &attribute, const QVariant &value ) /Deprecated/;
143143
%Docstring
144-
Notifies about changes of attributes
144+
Notifies about changes of attributes, this signal is not emitted when the value is set
145+
back to the original one.
145146

146147
:param attribute: The name of the attribute that changed.
147148
:param value: The new value of the attribute.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "get_current_form_field_value",
3+
"type": "function",
4+
"description": "Returns the current value of a field in the form or table row currently being edited.",
5+
"arguments": [ {"arg":"field_name","description":"a field name in the current form or table row"}],
6+
"examples": [ { "expression":"get_current_form_field_value( 'FIELD_NAME' )","returns":"The current value of field 'FIELD_NAME'."} ]
7+
}

src/core/expression/qgsexpression.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,9 @@ void QgsExpression::initVariableHelp()
771771

772772
//provider notification
773773
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)." ) );
774+
775+
//form context variable
776+
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." ) );
774777
}
775778

776779
QString QgsExpression::variableHelpText( const QString &variableName )

src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
#include <QSettings>
2323

24+
QString QgsValueRelationFieldFormatter::FORM_SCOPE_FUNCTIONS_RE = QStringLiteral( "%1\\s*\\(\\s*'([^']+)'\\s*\\)" );
25+
2426
bool orderByKeyLessThan( const QgsValueRelationFieldFormatter::ValueRelationItem &p1, const QgsValueRelationFieldFormatter::ValueRelationItem &p2 )
2527
{
2628
return qgsVariantLessThan( p1.key, p2.key );
@@ -99,7 +101,7 @@ QVariant QgsValueRelationFieldFormatter::createCache( QgsVectorLayer *layer, int
99101

100102
}
101103

102-
QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatter::createCache( const QVariantMap &config )
104+
QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatter::createCache( const QVariantMap &config, const QgsFeature &formFeature )
103105
{
104106
ValueRelationCache cache;
105107

@@ -116,11 +118,19 @@ QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatte
116118

117119
request.setFlags( QgsFeatureRequest::NoGeometry );
118120
request.setSubsetOfAttributes( QgsAttributeList() << ki << vi );
119-
if ( !config.value( QStringLiteral( "FilterExpression" ) ).toString().isEmpty() )
121+
122+
const QString expression = config.value( QStringLiteral( "FilterExpression" ) ).toString();
123+
124+
// Skip the filter and build a full cache if the form scope is required and the feature
125+
// is not valid or the attributes required for the filter have no valid value
126+
if ( ! expression.isEmpty() && ( ! expressionRequiresFormScope( expression )
127+
|| expressionIsUsable( expression, formFeature ) ) )
120128
{
121129
QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) );
130+
if ( formFeature.isValid( ) )
131+
context.appendScope( QgsExpressionContextUtils::formScope( formFeature ) );
122132
request.setExpressionContext( context );
123-
request.setFilterExpression( config.value( QStringLiteral( "FilterExpression" ) ).toString() );
133+
request.setFilterExpression( expression );
124134
}
125135

126136
QgsFeatureIterator fit = layer->getFeatures( request );
@@ -162,3 +172,58 @@ QStringList QgsValueRelationFieldFormatter::valueToStringList( const QVariant &v
162172
}
163173
return checkList;
164174
}
175+
176+
177+
QSet<QString> QgsValueRelationFieldFormatter::expressionFormVariables( const QString &expression )
178+
{
179+
const QStringList formVariables( QgsExpressionContextUtils::formScope()->variableNames() );
180+
QSet<QString> variables;
181+
182+
for ( auto it = formVariables.constBegin(); it != formVariables.constEnd(); it++ )
183+
{
184+
if ( expression.contains( *it ) )
185+
{
186+
variables.insert( *it );
187+
}
188+
}
189+
return variables;
190+
}
191+
192+
bool QgsValueRelationFieldFormatter::expressionRequiresFormScope( const QString &expression )
193+
{
194+
return !( expressionFormAttributes( expression ).isEmpty() && expressionFormVariables( expression ).isEmpty() );
195+
}
196+
197+
QSet<QString> QgsValueRelationFieldFormatter::expressionFormAttributes( const QString &expression )
198+
{
199+
QSet<QString> attributes;
200+
const QStringList formFunctions( QgsExpressionContextUtils::formScope()->functionNames() );
201+
QRegularExpression re;
202+
for ( const auto &fname : formFunctions )
203+
{
204+
if ( QgsExpressionContextUtils::formScope()->function( fname )->parameters().count( ) != 0 )
205+
{
206+
re.setPattern( QgsValueRelationFieldFormatter::FORM_SCOPE_FUNCTIONS_RE.arg( fname ) );
207+
QRegularExpressionMatchIterator i = re.globalMatch( expression );
208+
while ( i.hasNext() )
209+
{
210+
QRegularExpressionMatch match = i.next();
211+
attributes.insert( match.captured( 1 ) );
212+
}
213+
}
214+
}
215+
return attributes;
216+
}
217+
218+
bool QgsValueRelationFieldFormatter::expressionIsUsable( const QString &expression, const QgsFeature &feature )
219+
{
220+
const QSet<QString> attrs = expressionFormAttributes( expression );
221+
for ( auto it = attrs.constBegin() ; it != attrs.constEnd(); it++ )
222+
{
223+
if ( ! feature.attribute( *it ).isValid() )
224+
return false;
225+
}
226+
if ( ! expressionFormVariables( expression ).isEmpty() && feature.geometry().isEmpty( ) )
227+
return false;
228+
return true;
229+
}

src/core/fieldformatter/qgsvaluerelationfieldformatter.h

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@
1818

1919
#include "qgis_core.h"
2020
#include "qgsfieldformatter.h"
21+
#include "qgsexpression.h"
22+
#include "qgsexpressioncontext.h"
2123

2224
#include <QVector>
2325
#include <QVariant>
2426

27+
2528
/**
2629
* \ingroup core
2730
* Field formatter for a value relation field.
@@ -62,23 +65,71 @@ class CORE_EXPORT QgsValueRelationFieldFormatter : public QgsFieldFormatter
6265

6366
QVariant createCache( QgsVectorLayer *layer, int fieldIndex, const QVariantMap &config ) const override;
6467

68+
/**
69+
* Utility to convert an array or a string representation of and array \a value to a string list
70+
*
71+
* \param value The value to be converted
72+
* \return A string list
73+
* \since QGIS 3.2
74+
*/
75+
static QStringList valueToStringList( const QVariant &value );
76+
6577
/**
6678
* Create a cache for a value relation field.
6779
* This can be used to keep the value map in the local memory
6880
* if doing multiple lookups in a loop.
81+
* \param config The widget configuration
82+
* \param formFeature The feature currently being edited with current attribute values
83+
* \return A kvp list of values for the widget
6984
*
7085
* \since QGIS 3.0
7186
*/
72-
static QgsValueRelationFieldFormatter::ValueRelationCache createCache( const QVariantMap &config );
87+
static QgsValueRelationFieldFormatter::ValueRelationCache createCache( const QVariantMap &config, const QgsFeature &formFeature = QgsFeature() );
7388

7489
/**
75-
* Utility to convert an array or a string representation of and array \a value to a string list
90+
* Check if the \a expression requires a form scope (i.e. if it uses fields
91+
* or geometry of the currently edited feature).
7692
*
77-
* \param value The value to be converted
78-
* \return A string list
93+
* \param expression The widget's filter expression
94+
* \return true if the expression requires a form scope
7995
* \since QGIS 3.2
8096
*/
81-
static QStringList valueToStringList( const QVariant &value );
97+
static bool expressionRequiresFormScope( const QString &expression );
98+
99+
/**
100+
* Return a list of attributes required by the form context \a expression
101+
*
102+
* \param expression Form filter expression
103+
* \return list of attributes required by the expression
104+
* \since QGIS 3.2
105+
*/
106+
static QSet<QString> expressionFormAttributes( const QString &expression );
107+
108+
/**
109+
* Return a list of variables required by the form context \a expression
110+
*
111+
* \param expression Form filter expression
112+
* \return list of variables required by the expression
113+
* \since QGIS 3.2
114+
*/
115+
static QSet<QString> expressionFormVariables( const QString &expression );
116+
117+
/**
118+
* Check wether the \a feature has all values required by the \a expression
119+
*
120+
* @return True if the expression can be used
121+
* \since QGIS 3.2
122+
*/
123+
static bool expressionIsUsable( const QString &expression, const QgsFeature &feature );
124+
125+
/**
126+
* Regular expression to find dynamic filtering based on form field values
127+
* \see GetCurrentFormFieldValue()
128+
*
129+
* \since QGIS 3.2
130+
*/
131+
static QString FORM_SCOPE_FUNCTIONS_RE;
132+
82133
};
83134

84135
Q_DECLARE_METATYPE( QgsValueRelationFieldFormatter::ValueRelationCache )

0 commit comments

Comments
 (0)