[core] Address review: Move loadFunctionsFromProject and cleanFunctionsFromProject to QgsProject::read and ::clear, respectively. Introduce function pythonEmbeddedInProjectAllowed to ask for permissions to load both macros and expression functions, with a parameter enum and rename existing enum for macros. Sort includes. Drop not needed file. Add python/expressions/ to gitignore, to get rid of files produced by the introduced Python test.

This commit is contained in:
Germán Carrillo 2024-09-05 22:31:45 -05:00
parent 053e95e5e6
commit c5286e4dcc
25 changed files with 294 additions and 329 deletions

15
.gitignore vendored
View File

@ -47,6 +47,21 @@ desktop.ini
doc/INSTALL.tex
i18n/*.qm
ms-windows/*.exe*
ms-windows/Installer-Files/postinstall.bat
ms-windows/Installer-Files/preremove.bat
ms-windows/nsis/
ms-windows/osgeo4w/addons/
ms-windows/osgeo4w/binary-*
ms-windows/osgeo4w/build-*
ms-windows/osgeo4w/nsis/
ms-windows/osgeo4w/packages-x86/
ms-windows/osgeo4w/packages-x86_64/
ms-windows/osgeo4w/unpacked/
ms-windows/osgeo4w/untgz/
ms-windows/packages/
ms-windows/progs/
ms-windows/untgz/
python/expressions/
python/plugins/grassprovider/description/algorithms.json
python/plugins/grassprovider/tests/testdata/directions.tif.aux.xml
python/plugins/processing/tests/testdata/*.aux.xml

View File

@ -620,34 +620,37 @@ Qgis.VectorLayerTypeFlags = lambda flags=0: Qgis.VectorLayerTypeFlag(flags)
Qgis.VectorLayerTypeFlags.baseClass = Qgis
VectorLayerTypeFlags = Qgis # dirty hack since SIP seems to introduce the flags in module
# monkey patching scoped based enum
Qgis.Never = Qgis.PythonMacroMode.Never
Qgis.Never.is_monkey_patched = True
Qgis.Never.__doc__ = "Macros are never run"
Qgis.Ask = Qgis.PythonMacroMode.Ask
Qgis.Ask.is_monkey_patched = True
Qgis.Ask.__doc__ = "User is prompt before running"
Qgis.SessionOnly = Qgis.PythonMacroMode.SessionOnly
Qgis.SessionOnly.is_monkey_patched = True
Qgis.SessionOnly.__doc__ = "Only during this session"
Qgis.Always = Qgis.PythonMacroMode.Always
Qgis.Always.is_monkey_patched = True
Qgis.Always.__doc__ = "Macros are always run"
Qgis.NotForThisSession = Qgis.PythonMacroMode.NotForThisSession
Qgis.NotForThisSession.is_monkey_patched = True
Qgis.NotForThisSession.__doc__ = "Macros will not be run for this session"
Qgis.PythonMacroMode.__doc__ = """Authorisation to run Python Macros
Qgis.PythonEmbeddedMode.Never.__doc__ = "Python embedded never run"
Qgis.PythonEmbeddedMode.Ask.__doc__ = "User is prompt before running"
Qgis.PythonEmbeddedMode.SessionOnly.__doc__ = "Only during this session"
Qgis.PythonEmbeddedMode.Always.__doc__ = "Python embedded is always run"
Qgis.PythonEmbeddedMode.NotForThisSession.__doc__ = "Python embedded will not be run for this session"
Qgis.PythonEmbeddedMode.__doc__ = """Authorisation to run Python Embedded in projects
.. versionadded:: 3.10
.. versionadded:: 3.40
* ``Never``: Macros are never run
* ``Never``: Python embedded never run
* ``Ask``: User is prompt before running
* ``SessionOnly``: Only during this session
* ``Always``: Macros are always run
* ``NotForThisSession``: Macros will not be run for this session
* ``Always``: Python embedded is always run
* ``NotForThisSession``: Python embedded will not be run for this session
"""
# --
Qgis.PythonMacroMode.baseClass = Qgis
Qgis.PythonEmbeddedMode.baseClass = Qgis
# monkey patching scoped based enum
Qgis.PythonEmbeddedType.Macro.__doc__ = ""
Qgis.PythonEmbeddedType.ExpressionFunction.__doc__ = ""
Qgis.PythonEmbeddedType.__doc__ = """Type of Python Embedded in projects
.. versionadded:: 3.40
* ``Macro``:
* ``ExpressionFunction``:
"""
# --
Qgis.PythonEmbeddedType.baseClass = Qgis
QgsDataProvider.ReadFlag = Qgis.DataProviderReadFlag
# monkey patching scoped based enum
QgsDataProvider.FlagTrustDataSource = Qgis.DataProviderReadFlag.TrustDataSource

View File

@ -519,8 +519,6 @@ Returns the number of functions defined in the parser
:return: The number of function defined in the parser.
%End
static QString quotedColumnRef( QString name );
%Docstring
Returns a quoted column reference (in double quotes)

View File

@ -1657,6 +1657,9 @@ Sets the elevation shading renderer used for global map shading
.. versionadded:: 3.30
%End
SIP_PYOBJECT __repr__();
%MethodCode
QString str = QStringLiteral( "<QgsProject: '%1'%2>" ).arg( sipCpp->fileName(),

View File

@ -224,8 +224,8 @@ The development version
typedef QFlags<Qgis::VectorLayerTypeFlag> VectorLayerTypeFlags;
enum class PythonMacroMode /BaseType=IntEnum/
{
enum class PythonEmbeddedMode /BaseType=IntEnum/
{
Never,
Ask,
SessionOnly,
@ -233,6 +233,12 @@ The development version
NotForThisSession,
};
enum class PythonEmbeddedType /BaseType=IntEnum/
{
Macro,
ExpressionFunction,
};
enum class DataProviderReadFlag /BaseType=IntFlag/
{
TrustDataSource,

View File

@ -251,7 +251,6 @@ Returns the screen at the given global ``point`` (pixel).
signals:
void optionsChanged();

View File

@ -612,34 +612,37 @@ Qgis.VectorLayerTypeFlag.baseClass = Qgis
Qgis.VectorLayerTypeFlags.baseClass = Qgis
VectorLayerTypeFlags = Qgis # dirty hack since SIP seems to introduce the flags in module
# monkey patching scoped based enum
Qgis.Never = Qgis.PythonMacroMode.Never
Qgis.Never.is_monkey_patched = True
Qgis.Never.__doc__ = "Macros are never run"
Qgis.Ask = Qgis.PythonMacroMode.Ask
Qgis.Ask.is_monkey_patched = True
Qgis.Ask.__doc__ = "User is prompt before running"
Qgis.SessionOnly = Qgis.PythonMacroMode.SessionOnly
Qgis.SessionOnly.is_monkey_patched = True
Qgis.SessionOnly.__doc__ = "Only during this session"
Qgis.Always = Qgis.PythonMacroMode.Always
Qgis.Always.is_monkey_patched = True
Qgis.Always.__doc__ = "Macros are always run"
Qgis.NotForThisSession = Qgis.PythonMacroMode.NotForThisSession
Qgis.NotForThisSession.is_monkey_patched = True
Qgis.NotForThisSession.__doc__ = "Macros will not be run for this session"
Qgis.PythonMacroMode.__doc__ = """Authorisation to run Python Macros
Qgis.PythonEmbeddedMode.Never.__doc__ = "Python embedded never run"
Qgis.PythonEmbeddedMode.Ask.__doc__ = "User is prompt before running"
Qgis.PythonEmbeddedMode.SessionOnly.__doc__ = "Only during this session"
Qgis.PythonEmbeddedMode.Always.__doc__ = "Python embedded is always run"
Qgis.PythonEmbeddedMode.NotForThisSession.__doc__ = "Python embedded will not be run for this session"
Qgis.PythonEmbeddedMode.__doc__ = """Authorisation to run Python Embedded in projects
.. versionadded:: 3.10
.. versionadded:: 3.40
* ``Never``: Macros are never run
* ``Never``: Python embedded never run
* ``Ask``: User is prompt before running
* ``SessionOnly``: Only during this session
* ``Always``: Macros are always run
* ``NotForThisSession``: Macros will not be run for this session
* ``Always``: Python embedded is always run
* ``NotForThisSession``: Python embedded will not be run for this session
"""
# --
Qgis.PythonMacroMode.baseClass = Qgis
Qgis.PythonEmbeddedMode.baseClass = Qgis
# monkey patching scoped based enum
Qgis.PythonEmbeddedType.Macro.__doc__ = ""
Qgis.PythonEmbeddedType.ExpressionFunction.__doc__ = ""
Qgis.PythonEmbeddedType.__doc__ = """Type of Python Embedded in projects
.. versionadded:: 3.40
* ``Macro``:
* ``ExpressionFunction``:
"""
# --
Qgis.PythonEmbeddedType.baseClass = Qgis
QgsDataProvider.ReadFlag = Qgis.DataProviderReadFlag
# monkey patching scoped based enum
QgsDataProvider.FlagTrustDataSource = Qgis.DataProviderReadFlag.TrustDataSource

View File

@ -519,8 +519,6 @@ Returns the number of functions defined in the parser
:return: The number of function defined in the parser.
%End
static QString quotedColumnRef( QString name );
%Docstring
Returns a quoted column reference (in double quotes)

View File

@ -1657,6 +1657,9 @@ Sets the elevation shading renderer used for global map shading
.. versionadded:: 3.30
%End
SIP_PYOBJECT __repr__();
%MethodCode
QString str = QStringLiteral( "<QgsProject: '%1'%2>" ).arg( sipCpp->fileName(),

View File

@ -224,8 +224,8 @@ The development version
typedef QFlags<Qgis::VectorLayerTypeFlag> VectorLayerTypeFlags;
enum class PythonMacroMode
{
enum class PythonEmbeddedMode
{
Never,
Ask,
SessionOnly,
@ -233,6 +233,12 @@ The development version
NotForThisSession,
};
enum class PythonEmbeddedType
{
Macro,
ExpressionFunction,
};
enum class DataProviderReadFlag
{
TrustDataSource,

View File

@ -251,7 +251,6 @@ Returns the screen at the given global ``point`` (pixel).
signals:
void optionsChanged();

View File

@ -230,11 +230,11 @@ QgsOptions::QgsOptions( QWidget *parent, Qt::WindowFlags fl, const QList<QgsOpti
// non-default themes are best rendered using the Fusion style, therefore changing themes must require a restart to
lblUITheme->setText( QStringLiteral( "%1 <i>(%2)</i>" ).arg( lblUITheme->text(), tr( "QGIS restart required" ) ) );
mEnableMacrosComboBox->addItem( tr( "Never" ), QVariant::fromValue( Qgis::PythonMacroMode::Never ) );
mEnableMacrosComboBox->addItem( tr( "Ask" ), QVariant::fromValue( Qgis::PythonMacroMode::Ask ) );
mEnableMacrosComboBox->addItem( tr( "For This Session Only" ), QVariant::fromValue( Qgis::PythonMacroMode::SessionOnly ) );
mEnableMacrosComboBox->addItem( tr( "Not During This Session" ), QVariant::fromValue( Qgis::PythonMacroMode::NotForThisSession ) );
mEnableMacrosComboBox->addItem( tr( "Always (Not Recommended)" ), QVariant::fromValue( Qgis::PythonMacroMode::Always ) );
mEnableMacrosComboBox->addItem( tr( "Never" ), QVariant::fromValue( Qgis::PythonEmbeddedMode::Never ) );
mEnableMacrosComboBox->addItem( tr( "Ask" ), QVariant::fromValue( Qgis::PythonEmbeddedMode::Ask ) );
mEnableMacrosComboBox->addItem( tr( "For This Session Only" ), QVariant::fromValue( Qgis::PythonEmbeddedMode::SessionOnly ) );
mEnableMacrosComboBox->addItem( tr( "Not During This Session" ), QVariant::fromValue( Qgis::PythonEmbeddedMode::NotForThisSession ) );
mEnableMacrosComboBox->addItem( tr( "Always (Not Recommended)" ), QVariant::fromValue( Qgis::PythonEmbeddedMode::Always ) );
mIdentifyHighlightColorButton->setColorDialogTitle( tr( "Identify Highlight Color" ) );
mIdentifyHighlightColorButton->setAllowOpacity( true );
@ -823,8 +823,8 @@ QgsOptions::QgsOptions( QWidget *parent, Qt::WindowFlags fl, const QList<QgsOpti
chbAskToSaveProjectChanges->setChecked( mSettings->value( QStringLiteral( "qgis/askToSaveProjectChanges" ), QVariant( true ) ).toBool() );
mLayerDeleteConfirmationChkBx->setChecked( mSettings->value( QStringLiteral( "qgis/askToDeleteLayers" ), true ).toBool() );
chbWarnOldProjectVersion->setChecked( mSettings->value( QStringLiteral( "/qgis/warnOldProjectVersion" ), QVariant( true ) ).toBool() );
Qgis::PythonMacroMode pyMacroMode = mSettings->enumValue( QStringLiteral( "/qgis/enableMacros" ), Qgis::PythonMacroMode::Ask );
mEnableMacrosComboBox->setCurrentIndex( mEnableMacrosComboBox->findData( QVariant::fromValue( pyMacroMode ) ) );
Qgis::PythonEmbeddedMode pyEmbeddedMode = mSettings->enumValue( QStringLiteral( "/qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::Ask );
mEnableMacrosComboBox->setCurrentIndex( mEnableMacrosComboBox->findData( QVariant::fromValue( pyEmbeddedMode ) ) );
mDefaultPathsComboBox->addItem( tr( "Absolute" ), static_cast< int >( Qgis::FilePathType::Absolute ) );
mDefaultPathsComboBox->addItem( tr( "Relative" ), static_cast< int >( Qgis::FilePathType::Relative ) );
@ -1655,7 +1655,7 @@ void QgsOptions::saveOptions()
mSettings->setValue( QStringLiteral( "/qgis/projectTemplateDir" ), leTemplateFolder->text() );
QgisApp::instance()->updateProjectFromTemplates();
}
mSettings->setEnumValue( QStringLiteral( "/qgis/enableMacros" ), mEnableMacrosComboBox->currentData().value<Qgis::PythonMacroMode>() );
mSettings->setEnumValue( QStringLiteral( "/qgis/enablePythonEmbedded" ), mEnableMacrosComboBox->currentData().value<Qgis::PythonEmbeddedMode>() );
mSettings->setValue( QStringLiteral( "/qgis/defaultProjectPathsRelative" ),
static_cast< Qgis::FilePathType >( mDefaultPathsComboBox->currentData().toInt() ) == Qgis::FilePathType::Relative );

View File

@ -2889,17 +2889,17 @@ void QgisApp::readSettings()
readRecentProjects();
// this is a new session, reset enable macros value when they are set for session
Qgis::PythonMacroMode macroMode = settings.enumValue( QStringLiteral( "qgis/enableMacros" ), Qgis::PythonMacroMode::Ask );
switch ( macroMode )
Qgis::PythonEmbeddedMode pythonEmbeddedMode = settings.enumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::Ask );
switch ( pythonEmbeddedMode )
{
case Qgis::PythonMacroMode::NotForThisSession:
case Qgis::PythonMacroMode::SessionOnly:
settings.setEnumValue( QStringLiteral( "qgis/enableMacros" ), Qgis::PythonMacroMode::Ask );
case Qgis::PythonEmbeddedMode::NotForThisSession:
case Qgis::PythonEmbeddedMode::SessionOnly:
settings.setEnumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::Ask );
break;
case Qgis::PythonMacroMode::Always:
case Qgis::PythonMacroMode::Never:
case Qgis::PythonMacroMode::Ask:
case Qgis::PythonEmbeddedMode::Always:
case Qgis::PythonEmbeddedMode::Never:
case Qgis::PythonEmbeddedMode::Ask:
break;
}
}
@ -6575,14 +6575,14 @@ bool QgisApp::addProject( const QString &projectFile )
if ( !QgsProject::instance()->readEntry( QStringLiteral( "Macros" ), QStringLiteral( "/pythonCode" ), QString() ).isEmpty() )
{
auto lambda = []() {QgisApp::instance()->enableProjectMacros();};
QgsGui::pythonMacroAllowed( lambda, mInfoBar );
QgsGui::pythonEmbeddedInProjectAllowed( lambda, mInfoBar, Qgis::PythonEmbeddedType::Macro );
}
// does the project have expression functions?
const QString projectFunctions = QgsProject::instance()->readEntry( QStringLiteral( "ExpressionFunctions" ), QStringLiteral( "/pythonCode" ), QString() );
if ( !projectFunctions.isEmpty() )
{
QgsGui::pythonExpressionFromProjectAllowed( mInfoBar );
QgsGui::pythonEmbeddedInProjectAllowed( nullptr, mInfoBar, Qgis::PythonEmbeddedType::ExpressionFunction );
}
}
#endif
@ -13865,15 +13865,6 @@ void QgisApp::closeProject()
}
mPythonMacrosEnabled = false;
#ifdef WITH_BINDINGS
// unload the project expression functions and reload user expressions
const QString projectFunctions = QgsProject::instance()->readEntry( QStringLiteral( "ExpressionFunctions" ), QStringLiteral( "/pythonCode" ), QString() );
if ( !projectFunctions.isEmpty() )
{
QgsExpression::cleanFunctionsFromProject();
}
#endif
mLegendExpressionFilterButton->setExpressionText( QString() );
mLegendExpressionFilterButton->setChecked( false );
mFilterLegendByMapContentAction->setChecked( false );

View File

@ -630,24 +630,6 @@ class CORE_EXPORT QgsExpression
*/
static int functionCount();
/**
* Loads python expression functions stored in the currrent project
* \returns Whether the project functions where loaded or not.
*
* \note not available in Python bindings
* \since QGIS 3.40
*/
static bool loadFunctionsFromProject() SIP_SKIP;
/**
* Unloads python expression functions stored in the current project
* and reloads local functions from the user profile.
*
* \note not available in Python bindings
* \since QGIS 3.40
*/
static void cleanFunctionsFromProject() SIP_SKIP;
/**
* Returns a quoted column reference (in double quotes)
* \see quotedString()

View File

@ -64,7 +64,6 @@
#include "qgsunittypes.h"
#include "qgsspatialindex.h"
#include "qgscolorrampimpl.h"
#include "qgspythonrunner.h"
#include <QMimeDatabase>
#include <QProcessEnvironment>
@ -9502,22 +9501,6 @@ const QStringList &QgsExpression::BuiltinFunctions()
return *sBuiltinFunctions();
}
bool QgsExpression::loadFunctionsFromProject()
{
const QString projectFunctions = QgsProject::instance()->readEntry( QStringLiteral( "ExpressionFunctions" ), QStringLiteral( "/pythonCode" ), QString() );
if ( !projectFunctions.isEmpty() )
{
QgsPythonRunner::run( projectFunctions );
return true;
}
return false;
}
void QgsExpression::cleanFunctionsFromProject()
{
QgsPythonRunner::run( "qgis.utils.clean_project_expression_functions()" );
}
QgsArrayForeachExpressionFunction::QgsArrayForeachExpressionFunction()
: QgsExpressionFunction( QStringLiteral( "array_foreach" ), QgsExpressionFunction::ParameterList() // skip-keyword-check
<< QgsExpressionFunction::Parameter( QStringLiteral( "array" ) )

View File

@ -74,6 +74,7 @@
#include "qgsrunnableprovidercreator.h"
#include "qgssettingsregistrycore.h"
#include "qgspluginlayer.h"
#include "qgspythonrunner.h"
#include <algorithm>
#include <QApplication>
@ -1167,6 +1168,15 @@ void QgsProject::clear()
emit aboutToBeCleared();
if ( !mIsBeingDeleted )
{
// Unregister expression functions stored in the project.
// If we clean on destruction we may end-up with a non-valid
// mPythonUtils, so be safe and only clean when not destroying.
// This should be called before calling mProperties.clearKeys().
cleanFunctionsFromProject();
}
mProjectScope.reset();
mFile.setFileName( QString() );
mProperties.clearKeys();
@ -2284,6 +2294,11 @@ bool QgsProject::readProjectFile( const QString &filename, Qgis::ProjectReadFlag
QgsMessageLog::logMessage( tr( "Project Variables Invalid" ), tr( "The project contains invalid variable settings." ) );
}
// Register expression functions stored in the project.
// They might be using project variables and might be
// in turn being used by other components (e.g., layouts).
loadFunctionsFromProject();
QDomElement element = doc->documentElement().firstChildElement( QStringLiteral( "projectMetadata" ) );
if ( !element.isNull() )
@ -5266,6 +5281,33 @@ void QgsProject::loadProjectFlags( const QDomDocument *doc )
setFlags( flags );
}
bool QgsProject::loadFunctionsFromProject( bool force )
{
if ( QgsPythonRunner::isValid() )
{
const Qgis::PythonEmbeddedMode pythonEmbeddedMode = QgsSettings().enumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::Ask );
if ( force || pythonEmbeddedMode == Qgis::PythonEmbeddedMode::SessionOnly || pythonEmbeddedMode == Qgis::PythonEmbeddedMode::Always )
{
const QString projectFunctions = readEntry( QStringLiteral( "ExpressionFunctions" ), QStringLiteral( "/pythonCode" ), QString() );
if ( !projectFunctions.isEmpty() )
{
QgsPythonRunner::run( projectFunctions );
return true;
}
}
}
return false;
}
void QgsProject::cleanFunctionsFromProject()
{
if ( QgsPythonRunner::isValid() )
{
QgsPythonRunner::run( "qgis.utils.clean_project_expression_functions()" );
}
}
/// @cond PRIVATE
GetNamedProjectColor::GetNamedProjectColor( const QgsProject *project )
: QgsScopedExpressionFunction( QStringLiteral( "project_color" ), 1, QStringLiteral( "Color" ) )

View File

@ -1698,6 +1698,26 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
*/
void setElevationShadingRenderer( const QgsElevationShadingRenderer &elevationShadingRenderer );
/**
* Loads python expression functions stored in the currrent project
* \param force Whether to check enablePythonEmbedded setting (default) or not.
* \returns Whether the project functions were loaded or not.
*
* \note not available in Python bindings
* \since QGIS 3.40
*/
bool loadFunctionsFromProject( bool force = false ) SIP_SKIP;
/**
* Unloads python expression functions stored in the current project
* and reloads local functions from the user profile.
*
* \note not available in Python bindings
* \since QGIS 3.40
*/
void cleanFunctionsFromProject() SIP_SKIP;
#ifdef SIP_RUN
SIP_PYOBJECT __repr__();
% MethodCode

View File

@ -333,18 +333,29 @@ class CORE_EXPORT Qgis
Q_FLAG( VectorLayerTypeFlags )
/**
* Authorisation to run Python Macros
* \since QGIS 3.10
* Authorisation to run Python Embedded in projects
* \since QGIS 3.40
*/
enum class PythonMacroMode SIP_MONKEYPATCH_SCOPEENUM_UNNEST( Qgis, PythonMacroMode ) : int
{
Never = 0, //!< Macros are never run
enum class PythonEmbeddedMode : int
{
Never = 0, //!< Python embedded never run
Ask = 1, //!< User is prompt before running
SessionOnly = 2, //!< Only during this session
Always = 3, //!< Macros are always run
NotForThisSession, //!< Macros will not be run for this session
Always = 3, //!< Python embedded is always run
NotForThisSession, //!< Python embedded will not be run for this session
};
Q_ENUM( PythonMacroMode )
Q_ENUM( PythonEmbeddedMode )
/**
* Type of Python Embedded in projects
* \since QGIS 3.40
*/
enum class PythonEmbeddedType : int
{
Macro = 0,
ExpressionFunction = 1,
};
Q_ENUM( PythonEmbeddedType )
/**
* Flags which control data provider construction.

View File

@ -2356,7 +2356,7 @@ void QgsAttributeForm::initPython()
// If we have a function code, run it
if ( !initCode.isEmpty() )
{
if ( QgsGui::pythonMacroAllowed() )
if ( QgsGui::pythonEmbeddedInProjectAllowed( nullptr, nullptr, Qgis::PythonEmbeddedType::Macro ) )
QgsPythonRunner::run( initCode );
else
mMessageBar->pushMessage( QString(),

View File

@ -13,9 +13,10 @@
* *
***************************************************************************/
#include "qgsexpressionaddfunctionfiledialog.h"
#include <QPushButton>
#include <QStandardItemModel>
#include "qgsexpressionaddfunctionfiledialog.h"
QgsExpressionAddFunctionFileDialog::QgsExpressionAddFunctionFileDialog( bool enableProjectFunctions, QWidget *parent )
: QDialog( parent )

View File

@ -370,71 +370,121 @@ QgsGui::QgsGui()
qRegisterMetaType< QgsHistoryEntry >( "QgsHistoryEntry" );
}
bool QgsGui::pythonMacroAllowed( void ( *lambda )(), QgsMessageBar *messageBar )
bool QgsGui::pythonEmbeddedInProjectAllowed( void ( *lambda )(), QgsMessageBar *messageBar, Qgis::PythonEmbeddedType embeddedType )
{
const Qgis::PythonMacroMode macroMode = QgsSettings().enumValue( QStringLiteral( "qgis/enableMacros" ), Qgis::PythonMacroMode::Ask );
const Qgis::PythonEmbeddedMode pythonEmbeddedMode = QgsSettings().enumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::Ask );
switch ( macroMode )
switch ( pythonEmbeddedMode )
{
case Qgis::PythonMacroMode::SessionOnly:
case Qgis::PythonMacroMode::Always:
if ( lambda )
lambda();
case Qgis::PythonEmbeddedMode::SessionOnly:
case Qgis::PythonEmbeddedMode::Always:
if ( embeddedType == Qgis::PythonEmbeddedType::Macro )
{
if ( lambda )
lambda();
}
// If this is the case, expression functions
// are loaded directly by the QGIS project.
return true;
case Qgis::PythonMacroMode::Never:
case Qgis::PythonMacroMode::NotForThisSession:
case Qgis::PythonEmbeddedMode::Never:
case Qgis::PythonEmbeddedMode::NotForThisSession:
if ( messageBar )
{
messageBar->pushMessage( tr( "Python Macros" ),
tr( "Python macros are currently disabled and will not be run" ),
Qgis::MessageLevel::Warning );
switch ( embeddedType )
{
case Qgis::PythonEmbeddedType::Macro:
messageBar->pushMessage( tr( "Python Macros" ),
tr( "Python macros are currently disabled and will not be run" ),
Qgis::MessageLevel::Warning );
break;
case Qgis::PythonEmbeddedType::ExpressionFunction:
messageBar->pushMessage( tr( "Python Expressions" ),
tr( "Python expressions from project are currently disabled and will not be loaded" ),
Qgis::MessageLevel::Warning );
break;
}
}
return false;
case Qgis::PythonMacroMode::Ask:
if ( !lambda )
case Qgis::PythonEmbeddedMode::Ask:
if ( embeddedType == Qgis::PythonEmbeddedType::Macro )
{
QMessageBox msgBox( QMessageBox::Information, tr( "Python Macros" ),
tr( "Python macros are currently disabled. Do you allow this macro to run?" ) );
QAbstractButton *stopSessionButton = msgBox.addButton( tr( "Disable for this Session" ), QMessageBox::DestructiveRole );
msgBox.addButton( tr( "No" ), QMessageBox::NoRole );
QAbstractButton *yesButton = msgBox.addButton( tr( "Yes" ), QMessageBox::YesRole );
msgBox.exec();
QAbstractButton *clicked = msgBox.clickedButton();
if ( clicked == stopSessionButton )
if ( !lambda )
{
QgsSettings().setEnumValue( QStringLiteral( "qgis/enableMacros" ), Qgis::PythonMacroMode::NotForThisSession );
QMessageBox msgBox( QMessageBox::Information, tr( "Python Macros" ),
tr( "Python macros are currently disabled. Do you allow this macro to run?" ) );
QAbstractButton *stopSessionButton = msgBox.addButton( tr( "Disable for this Session" ), QMessageBox::DestructiveRole );
msgBox.addButton( tr( "No" ), QMessageBox::NoRole );
QAbstractButton *yesButton = msgBox.addButton( tr( "Yes" ), QMessageBox::YesRole );
msgBox.exec();
QAbstractButton *clicked = msgBox.clickedButton();
if ( clicked == stopSessionButton )
{
QgsSettings().setEnumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::NotForThisSession );
}
return clicked == yesButton;
}
else
{
// create the notification widget for macros
Q_ASSERT( messageBar );
if ( messageBar )
{
QToolButton *btnEnableMacros = new QToolButton();
btnEnableMacros->setText( tr( "Enable Macros" ) );
btnEnableMacros->setStyleSheet( QStringLiteral( "background-color: rgba(255, 255, 255, 0); color: black; text-decoration: underline;" ) );
btnEnableMacros->setCursor( Qt::PointingHandCursor );
btnEnableMacros->setSizePolicy( QSizePolicy::Maximum, QSizePolicy::Preferred );
QgsMessageBarItem *macroMsg = new QgsMessageBarItem(
tr( "Security warning" ),
tr( "Python macros cannot currently be run." ),
btnEnableMacros,
Qgis::MessageLevel::Warning,
0,
messageBar );
connect( btnEnableMacros, &QToolButton::clicked, messageBar, [ = ]()
{
lambda();
messageBar->popWidget( macroMsg );
} );
// display the macros notification widget
messageBar->pushItem( macroMsg );
}
return false;
}
return clicked == yesButton;
}
else
else if ( embeddedType == Qgis::PythonEmbeddedType::ExpressionFunction )
{
// create the notification widget for macros
// create the notification widget for expressions from project
Q_ASSERT( messageBar );
if ( messageBar )
{
QToolButton *btnEnableMacros = new QToolButton();
btnEnableMacros->setText( tr( "Enable Macros" ) );
btnEnableMacros->setStyleSheet( QStringLiteral( "background-color: rgba(255, 255, 255, 0); color: black; text-decoration: underline;" ) );
btnEnableMacros->setCursor( Qt::PointingHandCursor );
btnEnableMacros->setSizePolicy( QSizePolicy::Maximum, QSizePolicy::Preferred );
QToolButton *btnEnableExpressionsFromProject = new QToolButton();
btnEnableExpressionsFromProject->setText( tr( "Enable python expressions from project" ) );
btnEnableExpressionsFromProject->setStyleSheet( QStringLiteral( "background-color: rgba(255, 255, 255, 0); color: black; text-decoration: underline;" ) );
btnEnableExpressionsFromProject->setCursor( Qt::PointingHandCursor );
btnEnableExpressionsFromProject->setSizePolicy( QSizePolicy::Maximum, QSizePolicy::Preferred );
QgsMessageBarItem *macroMsg = new QgsMessageBarItem(
QgsMessageBarItem *expressionFromProjectMsg = new QgsMessageBarItem(
tr( "Security warning" ),
tr( "Python macros cannot currently be run." ),
btnEnableMacros,
tr( "Python expressions from project cannot currently be loaded." ),
btnEnableExpressionsFromProject,
Qgis::MessageLevel::Warning,
0,
messageBar );
connect( btnEnableMacros, &QToolButton::clicked, messageBar, [ = ]()
connect( btnEnableExpressionsFromProject, &QToolButton::clicked, messageBar, [ = ]()
{
lambda();
messageBar->popWidget( macroMsg );
QgsProject::instance()->loadFunctionsFromProject( true );
messageBar->popWidget( expressionFromProjectMsg );
} );
// display the macros notification widget
messageBar->pushItem( macroMsg );
// display the notification widget
messageBar->pushItem( expressionFromProjectMsg );
}
return false;
@ -443,59 +493,6 @@ bool QgsGui::pythonMacroAllowed( void ( *lambda )(), QgsMessageBar *messageBar )
return false;
}
bool QgsGui::pythonExpressionFromProjectAllowed( QgsMessageBar *messageBar )
{
const Qgis::PythonMacroMode pythonMode = QgsSettings().enumValue( QStringLiteral( "qgis/enableMacros" ), Qgis::PythonMacroMode::Ask );
switch ( pythonMode )
{
case Qgis::PythonMacroMode::SessionOnly:
case Qgis::PythonMacroMode::Always:
QgsExpression::loadFunctionsFromProject();
return true;
case Qgis::PythonMacroMode::Never:
case Qgis::PythonMacroMode::NotForThisSession:
if ( messageBar )
{
messageBar->pushMessage( tr( "Python Expressions" ),
tr( "Python expressions from project are currently disabled and will not be loaded" ),
Qgis::MessageLevel::Warning );
}
return false;
case Qgis::PythonMacroMode::Ask:
// create the notification widget for expressions from project
Q_ASSERT( messageBar );
if ( messageBar )
{
QToolButton *btnEnableExpressionsFromProject = new QToolButton();
btnEnableExpressionsFromProject->setText( tr( "Enable python expressions from project" ) );
btnEnableExpressionsFromProject->setStyleSheet( QStringLiteral( "background-color: rgba(255, 255, 255, 0); color: black; text-decoration: underline;" ) );
btnEnableExpressionsFromProject->setCursor( Qt::PointingHandCursor );
btnEnableExpressionsFromProject->setSizePolicy( QSizePolicy::Maximum, QSizePolicy::Preferred );
QgsMessageBarItem *expressionFromProjectMsg = new QgsMessageBarItem(
tr( "Security warning" ),
tr( "Python expressions from project cannot currently be loaded." ),
btnEnableExpressionsFromProject,
Qgis::MessageLevel::Warning,
0,
messageBar );
connect( btnEnableExpressionsFromProject, &QToolButton::clicked, messageBar, [ = ]()
{
QgsExpression::loadFunctionsFromProject();
messageBar->popWidget( expressionFromProjectMsg );
} );
// display the notification widget
messageBar->pushItem( expressionFromProjectMsg );
}
return false;
}
return false;
}
void QgsGui::initCalloutWidgets()
{
static std::once_flag initialized;

View File

@ -18,6 +18,7 @@
#ifndef QGSGUI_H
#define QGSGUI_H
#include "qgis.h"
#include "qgis_gui.h"
#include "qgis_sip.h"
#include <QWidget>
@ -291,28 +292,20 @@ class GUI_EXPORT QgsGui : public QObject
static QScreen *findScreenAt( QPoint point );
/**
* Returns TRUE if python macros are currently allowed to be run
* If the global option is to ask user, a modal dialog will be shown
* Returns TRUE if python embedded in a project is currently allowed to be loaded.
* If the global option is to ask user, a modal dialog will be shown for macros
* or a button to enable Python expressions will be shown in a message bar.
* \param lambda a pointer to a lambda method. If specified, the dialog is not modal,
* a message is shown with a button to enable macro.
* The lambda will be run either if macros are currently allowed or if the user accepts the message.
* The \a messageBar must be given in such case.
* \param messageBar the message bar must be provided if a lambda method is used.
*
* \note Not available in Python bindings
*/
static bool pythonMacroAllowed( void ( *lambda )() = nullptr, QgsMessageBar *messageBar = nullptr ) SIP_SKIP;
/**
* Returns TRUE if python expression functions from project are currently allowed to be loaded.
*
* If the global option is to ask user, a button to enable them will be shown in a message bar.
* \param messageBar Message bar to communicate with the user.
* \param embeddedType enum value to identify if macros or expression functions should be checked.
*
* \note Not available in Python bindings
* \since QGIS 3.40
*/
static bool pythonExpressionFromProjectAllowed( QgsMessageBar *messageBar ) SIP_SKIP;
static bool pythonEmbeddedInProjectAllowed( void ( *lambda )() = nullptr, QgsMessageBar *messageBar = nullptr, Qgis::PythonEmbeddedType embeddedType = Qgis::PythonEmbeddedType::Macro ) SIP_SKIP;
/**
* Initializes callout widgets.

View File

@ -1,96 +0,0 @@
/********************************************************************************
** Form generated from reading UI file 'qgsexpressionaddfunctionfiledialogbase.ui'
**
** Created by: Qt User Interface Compiler version 5.15.3
**
** WARNING! All changes made in this file will be lost when recompiling UI file!
********************************************************************************/
#ifndef QGSEXPRESSIONADDFUNCTIONFILEDIALOGBASE_H
#define QGSEXPRESSIONADDFUNCTIONFILEDIALOGBASE_H
#include <QtCore/QVariant>
#include <QtWidgets/QApplication>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QDialog>
#include <QtWidgets/QDialogButtonBox>
#include <QtWidgets/QGridLayout>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
QT_BEGIN_NAMESPACE
class Ui_QgsAddFunctionFileDialogBase
{
public:
QGridLayout *gridLayout;
QLabel *label;
QComboBox *cboFileOptions;
QLabel *lblNewFileName;
QDialogButtonBox *buttonBox;
QLineEdit *txtNewFileName;
void setupUi( QDialog *QgsAddFunctionFileDialogBase )
{
if ( QgsAddFunctionFileDialogBase->objectName().isEmpty() )
QgsAddFunctionFileDialogBase->setObjectName( QString::fromUtf8( "QgsAddFunctionFileDialogBase" ) );
QgsAddFunctionFileDialogBase->resize( 277, 132 );
gridLayout = new QGridLayout( QgsAddFunctionFileDialogBase );
gridLayout->setObjectName( QString::fromUtf8( "gridLayout" ) );
label = new QLabel( QgsAddFunctionFileDialogBase );
label->setObjectName( QString::fromUtf8( "label" ) );
gridLayout->addWidget( label, 0, 0, 1, 1 );
cboFileOptions = new QComboBox( QgsAddFunctionFileDialogBase );
cboFileOptions->addItem( QString() );
cboFileOptions->addItem( QString() );
cboFileOptions->setObjectName( QString::fromUtf8( "cboFileOptions" ) );
gridLayout->addWidget( cboFileOptions, 0, 1, 1, 1 );
lblNewFileName = new QLabel( QgsAddFunctionFileDialogBase );
lblNewFileName->setObjectName( QString::fromUtf8( "lblNewFileName" ) );
gridLayout->addWidget( lblNewFileName, 1, 0, 1, 1 );
buttonBox = new QDialogButtonBox( QgsAddFunctionFileDialogBase );
buttonBox->setObjectName( QString::fromUtf8( "buttonBox" ) );
buttonBox->setOrientation( Qt::Horizontal );
buttonBox->setStandardButtons( QDialogButtonBox::Cancel | QDialogButtonBox::Ok );
gridLayout->addWidget( buttonBox, 2, 0, 1, 2 );
txtNewFileName = new QLineEdit( QgsAddFunctionFileDialogBase );
txtNewFileName->setObjectName( QString::fromUtf8( "txtNewFileName" ) );
gridLayout->addWidget( txtNewFileName, 1, 1, 1, 1 );
retranslateUi( QgsAddFunctionFileDialogBase );
QObject::connect( buttonBox, SIGNAL( accepted() ), QgsAddFunctionFileDialogBase, SLOT( accept() ) );
QObject::connect( buttonBox, SIGNAL( rejected() ), QgsAddFunctionFileDialogBase, SLOT( reject() ) );
QMetaObject::connectSlotsByName( QgsAddFunctionFileDialogBase );
} // setupUi
void retranslateUi( QDialog *QgsAddFunctionFileDialogBase )
{
QgsAddFunctionFileDialogBase->setWindowTitle( QCoreApplication::translate( "QgsAddFunctionFileDialogBase", "Add Function File", nullptr ) );
label->setText( QCoreApplication::translate( "QgsAddFunctionFileDialogBase", "Create", nullptr ) );
cboFileOptions->setItemText( 0, QCoreApplication::translate( "QgsAddFunctionFileDialogBase", "Function file", nullptr ) );
cboFileOptions->setItemText( 1, QCoreApplication::translate( "QgsAddFunctionFileDialogBase", "Project functions", nullptr ) );
lblNewFileName->setText( QCoreApplication::translate( "QgsAddFunctionFileDialogBase", "File name", nullptr ) );
} // retranslateUi
};
namespace Ui
{
class QgsAddFunctionFileDialogBase: public Ui_QgsAddFunctionFileDialogBase {};
} // namespace Ui
QT_END_NAMESPACE
#endif // QGSEXPRESSIONADDFUNCTIONFILEDIALOGBASE_H

View File

@ -82,9 +82,17 @@ void TestQgsProjectExpressions::projectExpressions()
// Load expressions from project
// Project registers 2 functions: mychoice (overwriting it) and myprojectfunction
const QByteArray projectPath = QByteArray( TEST_DATA_DIR ) + "/projects/test_project_functions.qgz";
const Qgis::PythonEmbeddedMode pythonEmbeddedMode = QgsSettings().enumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::Ask );
QgsSettings().setEnumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::Never );
QgsProject::instance()->read( projectPath );
QCOMPARE( QgsExpression::functionIndex( QStringLiteral( "myprojectfunction" ) ), -1 );
QgsExpression::loadFunctionsFromProject();
// Set the global setting to accept expression functions
QgsSettings().setEnumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), Qgis::PythonEmbeddedMode::SessionOnly );
QgsProject::instance()->loadFunctionsFromProject();
QgsSettings().setEnumValue( QStringLiteral( "qgis/enablePythonEmbedded" ), pythonEmbeddedMode );
QVERIFY( QgsExpression::functionIndex( QStringLiteral( "myprojectfunction" ) ) != -1 );
QVERIFY( QgsExpression::functionIndex( QStringLiteral( "mychoice" ) ) != -1 ); // Overwritten function
const int count_project_loaded = QgsExpression::functionCount();
@ -95,7 +103,7 @@ void TestQgsProjectExpressions::projectExpressions()
QCOMPARE( exp.evaluate().toInt(), 2 ); // Different result because now it's from project
// Unload expressions from project, reload user ones
QgsExpression::cleanFunctionsFromProject();
QgsProject::instance()->cleanFunctionsFromProject();
const int count_project_unloaded = QgsExpression::functionCount();
QCOMPARE( count_before_project, count_project_unloaded ); // myprojectfunction is gone

View File

@ -133,7 +133,7 @@ class TestQgsEditFormConfig(QgisTestCase):
pyUrl = 'http://localhost:' + \
str(self.port) + '/qgis_local_server/layer_attribute_form.py'
QgsSettings().setEnumValue('qgis/enableMacros', Qgis.Always)
QgsSettings().setEnumValue('qgis/enablePythonEmbedded', Qgis.Always)
config.setInitFilePath(pyUrl)
config.setInitFunction('formOpen')