Skip to content

Commit

Permalink
[feature] Form context expressions in value relation widget
Browse files Browse the repository at this point in the history
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
  • Loading branch information
elpaso committed May 15, 2018
1 parent 4d36f37 commit 83328ae
Show file tree
Hide file tree
Showing 23 changed files with 607 additions and 83 deletions.
Expand Up @@ -8,6 +8,7 @@




class QgsValueRelationFieldFormatter : QgsFieldFormatter
{
%Docstring
Expand Down Expand Up @@ -56,25 +57,76 @@ Constructor for QgsValueRelationFieldFormatter.
virtual QVariant createCache( QgsVectorLayer *layer, int fieldIndex, const QVariantMap &config ) const;


static QgsValueRelationFieldFormatter::ValueRelationCache createCache( const QVariantMap &config );
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

.. 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 QStringList valueToStringList( const QVariant &value );
static bool expressionRequiresFormScope( const QString &expression );
%Docstring
Utility to convert an array or a string representation of and array ``value`` to a string list
Check if the ``expression`` requires a form scope (i.e. if it uses fields
or geometry of the currently edited feature).

:param value: The value to be converted
:param expression: The widget's filter expression

:return: A string list
:return: true if the expression requires a form scope

.. versionadded:: 3.2
%End

static QSet<QString> 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<QString> 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;

};


Expand Down
8 changes: 8 additions & 0 deletions python/core/auto_generated/qgsexpressioncontext.sip.in
Expand Up @@ -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 );
Expand Down
Expand Up @@ -279,6 +279,10 @@ change the visual cue.
.. versionadded:: 2.16
%End

protected:



};


Expand Down
19 changes: 19 additions & 0 deletions python/gui/auto_generated/qgsattributeeditorcontext.sip.in
Expand Up @@ -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


};

/************************************************************************
Expand Down
3 changes: 2 additions & 1 deletion python/gui/auto_generated/qgsattributeform.sip.in
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions 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'."} ]
}
3 changes: 3 additions & 0 deletions src/core/expression/qgsexpression.cpp
Expand Up @@ -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 )
Expand Down
71 changes: 68 additions & 3 deletions src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp
Expand Up @@ -21,6 +21,8 @@

#include <QSettings>

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 );
Expand Down Expand Up @@ -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;

Expand All @@ -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 );
Expand Down Expand Up @@ -162,3 +172,58 @@ QStringList QgsValueRelationFieldFormatter::valueToStringList( const QVariant &v
}
return checkList;
}


QSet<QString> QgsValueRelationFieldFormatter::expressionFormVariables( const QString &expression )
{
const QStringList formVariables( QgsExpressionContextUtils::formScope()->variableNames() );
QSet<QString> 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<QString> QgsValueRelationFieldFormatter::expressionFormAttributes( const QString &expression )
{
QSet<QString> 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<QString> 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;
}
61 changes: 56 additions & 5 deletions src/core/fieldformatter/qgsvaluerelationfieldformatter.h
Expand Up @@ -18,10 +18,13 @@

#include "qgis_core.h"
#include "qgsfieldformatter.h"
#include "qgsexpression.h"
#include "qgsexpressioncontext.h"

#include <QVector>
#include <QVariant>


/**
* \ingroup core
* Field formatter for a value relation field.
Expand Down Expand Up @@ -62,23 +65,71 @@ 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
* \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 );
static QgsValueRelationFieldFormatter::ValueRelationCache createCache( const QVariantMap &config, const QgsFeature &formFeature = QgsFeature() );

/**
* Utility to convert an array or a string representation of and array \a value to a string list
* Check if the \a expression requires a form scope (i.e. if it uses fields
* or geometry of the currently edited feature).
*
* \param value The value to be converted
* \return A string list
* \param expression The widget's filter expression
* \return true if the expression requires a form scope
* \since QGIS 3.2
*/
static QStringList valueToStringList( const QVariant &value );
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<QString> 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<QString> 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 )
Expand Down

0 comments on commit 83328ae

Please sign in to comment.