Skip to content

Commit 0328b7a

Browse files
authoredAug 1, 2017
Merge pull request #4943 from nyalldawson/exp_layer_rel
Add items for project map layers and relations to expression builder
2 parents a8f6dbc + 917263a commit 0328b7a

File tree

7 files changed

+312
-0
lines changed

7 files changed

+312
-0
lines changed
 

‎python/gui/qgsexpressionbuilderwidget.sip

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,30 @@ Sets the expression string for the widget
236236
Update the list of function files found at the given path
237237
%End
238238

239+
QStandardItemModel *model();
240+
%Docstring
241+
Returns a pointer to the dialog's function item model.
242+
This method is exposed for testing purposes only - it should not be used to modify the model.
243+
.. versionadded:: 3.0
244+
:rtype: QStandardItemModel
245+
%End
246+
247+
QgsProject *project();
248+
%Docstring
249+
Returns the project currently associated with the widget.
250+
.. seealso:: setProject()
251+
.. versionadded:: 3.0
252+
:rtype: QgsProject
253+
%End
254+
255+
void setProject( QgsProject *project );
256+
%Docstring
257+
Sets the ``project`` currently associated with the widget. This
258+
controls which layers and relations and other project-specific items are shown in the widget.
259+
.. seealso:: project()
260+
.. versionadded:: 3.0
261+
%End
262+
239263
public slots:
240264

241265
void loadSampleValues();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "Map Layers",
3+
"type": "group",
4+
"description": "Contains a list of map layers available in the current project."
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "Relations",
3+
"type": "group",
4+
"description": "Contains a list of relations available in the current project."
5+
}

‎src/gui/qgsexpressionbuilderwidget.cpp

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
#include "qgsfeatureiterator.h"
2626
#include "qgsvectorlayer.h"
2727
#include "qgssettings.h"
28+
#include "qgsproject.h"
29+
#include "qgsrelationmanager.h"
30+
#include "qgsrelation.h"
2831

2932
#include <QMenu>
3033
#include <QFile>
@@ -42,6 +45,7 @@ QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent )
4245
, mLayer( nullptr )
4346
, highlighter( nullptr )
4447
, mExpressionValid( false )
48+
, mProject( QgsProject::instance() )
4549
{
4650
setupUi( this );
4751

@@ -440,6 +444,32 @@ void QgsExpressionBuilderWidget::loadRecent( const QString &collection )
440444
}
441445
}
442446

447+
void QgsExpressionBuilderWidget::loadLayers()
448+
{
449+
if ( !mProject )
450+
return;
451+
452+
QMap<QString, QgsMapLayer *> layers = mProject->mapLayers();
453+
QMap<QString, QgsMapLayer *>::const_iterator layerIt = layers.constBegin();
454+
for ( ; layerIt != layers.constEnd(); ++layerIt )
455+
{
456+
registerItemForAllGroups( QStringList() << tr( "Map Layers" ), layerIt.value()->name(), QStringLiteral( "'%1'" ).arg( layerIt.key() ), formatLayerHelp( layerIt.value() ) );
457+
}
458+
}
459+
460+
void QgsExpressionBuilderWidget::loadRelations()
461+
{
462+
if ( !mProject )
463+
return;
464+
465+
QMap<QString, QgsRelation> relations = mProject->relationManager()->relations();
466+
QMap<QString, QgsRelation>::const_iterator relIt = relations.constBegin();
467+
for ( ; relIt != relations.constEnd(); ++relIt )
468+
{
469+
registerItemForAllGroups( QStringList() << tr( "Relations" ), relIt->name(), QStringLiteral( "'%1'" ).arg( relIt->id() ), formatRelationHelp( relIt.value() ) );
470+
}
471+
}
472+
443473
void QgsExpressionBuilderWidget::updateFunctionTree()
444474
{
445475
mModel->clear();
@@ -495,6 +525,12 @@ void QgsExpressionBuilderWidget::updateFunctionTree()
495525
registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText() );
496526
}
497527

528+
// load relation names
529+
loadRelations();
530+
531+
// load layer IDs
532+
loadLayers();
533+
498534
loadExpressionContext();
499535
}
500536

@@ -614,6 +650,36 @@ void QgsExpressionBuilderWidget::registerItemForAllGroups( const QStringList &gr
614650
}
615651
}
616652

653+
QString QgsExpressionBuilderWidget::formatRelationHelp( const QgsRelation &relation ) const
654+
{
655+
QString text = QStringLiteral( "<p>%1</p>" ).arg( tr( "Inserts the relation ID for the relation named '%1'." ).arg( relation.name() ) );
656+
text.append( QStringLiteral( "<p>%1</p>" ).arg( tr( "Current value: '%1'" ).arg( relation.id() ) ) );
657+
return text;
658+
}
659+
660+
QString QgsExpressionBuilderWidget::formatLayerHelp( const QgsMapLayer *layer ) const
661+
{
662+
QString text = QStringLiteral( "<p>%1</p>" ).arg( tr( "Inserts the layer ID for the layer named '%1'." ).arg( layer->name() ) );
663+
text.append( QStringLiteral( "<p>%1</p>" ).arg( tr( "Current value: '%1'" ).arg( layer->id() ) ) );
664+
return text;
665+
}
666+
667+
QStandardItemModel *QgsExpressionBuilderWidget::model()
668+
{
669+
return mModel;
670+
}
671+
672+
QgsProject *QgsExpressionBuilderWidget::project()
673+
{
674+
return mProject;
675+
}
676+
677+
void QgsExpressionBuilderWidget::setProject( QgsProject *project )
678+
{
679+
mProject = project;
680+
updateFunctionTree();
681+
}
682+
617683
void QgsExpressionBuilderWidget::showEvent( QShowEvent *e )
618684
{
619685
QWidget::showEvent( e );

‎src/gui/qgsexpressionbuilderwidget.h

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
class QgsFields;
3333
class QgsExpressionHighlighter;
34+
class QgsRelation;
3435

3536
/** \ingroup gui
3637
* An expression item that can be used in the QgsExpressionBuilderWidget tree.
@@ -225,6 +226,28 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp
225226
*/
226227
void updateFunctionFileList( const QString &path );
227228

229+
/**
230+
* Returns a pointer to the dialog's function item model.
231+
* This method is exposed for testing purposes only - it should not be used to modify the model.
232+
* \since QGIS 3.0
233+
*/
234+
QStandardItemModel *model();
235+
236+
/**
237+
* Returns the project currently associated with the widget.
238+
* \see setProject()
239+
* \since QGIS 3.0
240+
*/
241+
QgsProject *project();
242+
243+
/**
244+
* Sets the \a project currently associated with the widget. This
245+
* controls which layers and relations and other project-specific items are shown in the widget.
246+
* \see project()
247+
* \since QGIS 3.0
248+
*/
249+
void setProject( QgsProject *project );
250+
228251
public slots:
229252

230253
/**
@@ -286,6 +309,12 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp
286309

287310
void loadExpressionContext();
288311

312+
//! Loads current project relations names/id into the expression help tree
313+
void loadRelations();
314+
315+
//! Loads current project layer names/ids into the expression help tree
316+
void loadLayers();
317+
289318
/** Registers a node item for the expression builder, adding multiple items when the function exists in multiple groups
290319
* \param groups The groups the item will be show in the tree view. If a group doesn't exist it will be created.
291320
* \param label The label that is show to the user for the item in the tree.
@@ -300,6 +329,16 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp
300329
QgsExpressionItem::ItemType type = QgsExpressionItem::ExpressionNode,
301330
bool highlightedItem = false, int sortOrder = 1 );
302331

332+
/**
333+
* Returns a HTML formatted string for use as a \a relation item help.
334+
*/
335+
QString formatRelationHelp( const QgsRelation &relation ) const;
336+
337+
/**
338+
* Returns a HTML formatted string for use as a \a layer item help.
339+
*/
340+
QString formatLayerHelp( const QgsMapLayer *layer ) const;
341+
303342
bool mAutoSave;
304343
QString mFunctionsPath;
305344
QgsVectorLayer *mLayer = nullptr;
@@ -314,6 +353,7 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp
314353
QString mRecentKey;
315354
QMap<QString, QStringList> mFieldValues;
316355
QgsExpressionContext mExpressionContext;
356+
QPointer< QgsProject > mProject;
317357
};
318358

319359
#endif // QGSEXPRESSIONBUILDER_H

‎tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ ADD_PYTHON_TEST(PyQgsDistanceArea test_qgsdistancearea.py)
4747
ADD_PYTHON_TEST(PyQgsEditWidgets test_qgseditwidgets.py)
4848
ADD_PYTHON_TEST(PyQgsEllipsoidUtils test_qgsellipsoidutils.py)
4949
ADD_PYTHON_TEST(PyQgsExpression test_qgsexpression.py)
50+
ADD_PYTHON_TEST(PyQgsExpressionBuilderWidget test_qgsexpressionbuilderwidget.py)
5051
ADD_PYTHON_TEST(PyQgsExpressionLineEdit test_qgsexpressionlineedit.py)
5152
ADD_PYTHON_TEST(PyQgsExtentGroupBox test_qgsextentgroupbox.py)
5253
ADD_PYTHON_TEST(PyQgsFeature test_qgsfeature.py)
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit tests for QgsExpressionBuilderWidget
3+
4+
.. note:: This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
"""
9+
__author__ = 'Nyall Dawson'
10+
__date__ = '30/07/2017'
11+
__copyright__ = 'Copyright 2017, The QGIS Project'
12+
# This will get replaced with a git SHA1 when you do a git archive
13+
__revision__ = '$Format:%H$'
14+
15+
import qgis # NOQA
16+
17+
from qgis.PyQt.QtCore import Qt
18+
from qgis.testing import start_app, unittest
19+
from qgis.gui import QgsExpressionBuilderWidget
20+
from qgis.core import (QgsExpressionContext,
21+
QgsExpressionContextScope,
22+
QgsProject,
23+
QgsVectorLayer,
24+
QgsRelation,
25+
QgsFeature,
26+
QgsGeometry)
27+
start_app()
28+
29+
30+
def createReferencingLayer():
31+
layer = QgsVectorLayer("Point?field=fldtxt:string&field=foreignkey:integer",
32+
"referencinglayer", "memory")
33+
pr = layer.dataProvider()
34+
f1 = QgsFeature()
35+
f1.setFields(layer.pendingFields())
36+
f1.setAttributes(["test1", 123])
37+
f2 = QgsFeature()
38+
f2.setFields(layer.pendingFields())
39+
f2.setAttributes(["test2", 123])
40+
f3 = QgsFeature()
41+
f3.setFields(layer.pendingFields())
42+
f3.setAttributes(["foobar'bar", 124])
43+
assert pr.addFeatures([f1, f2, f3])
44+
return layer
45+
46+
47+
def createReferencedLayer():
48+
layer = QgsVectorLayer(
49+
"Point?field=x:string&field=y:integer&field=z:integer",
50+
"referencedlayer", "memory")
51+
pr = layer.dataProvider()
52+
f1 = QgsFeature()
53+
f1.setFields(layer.pendingFields())
54+
f1.setAttributes(["foo", 123, 321])
55+
f2 = QgsFeature()
56+
f2.setFields(layer.pendingFields())
57+
f2.setAttributes(["bar", 456, 654])
58+
f3 = QgsFeature()
59+
f3.setFields(layer.pendingFields())
60+
f3.setAttributes(["foobar'bar", 789, 554])
61+
assert pr.addFeatures([f1, f2, f3])
62+
return layer
63+
64+
65+
class TestQgsExpressionBuilderWidget(unittest.TestCase):
66+
67+
def setUp(self):
68+
self.referencedLayer = createReferencedLayer()
69+
self.referencingLayer = createReferencingLayer()
70+
QgsProject.instance().addMapLayers([self.referencedLayer, self.referencingLayer])
71+
72+
def testFunctionPresent(self):
73+
""" check through widget model to ensure it is initially populated with functions """
74+
w = QgsExpressionBuilderWidget()
75+
m = w.model()
76+
# check that some standard expression functions are shown
77+
items = m.findItems('lower', Qt.MatchRecursive)
78+
self.assertEqual(len(items), 1)
79+
items = m.findItems('upper', Qt.MatchRecursive)
80+
self.assertEqual(len(items), 1)
81+
items = m.findItems('asdasdasda#$@#$', Qt.MatchRecursive)
82+
self.assertEqual(len(items), 0)
83+
84+
def testVariables(self):
85+
""" check through widget model to ensure it is populated with variables """
86+
w = QgsExpressionBuilderWidget()
87+
m = w.model()
88+
89+
s = QgsExpressionContextScope()
90+
s.setVariable('my_var1', 'x')
91+
s.setVariable('my_var2', 'y')
92+
c = QgsExpressionContext()
93+
c.appendScope(s)
94+
95+
# check that variables are added when setting context
96+
w.setExpressionContext(c)
97+
items = m.findItems('my_var1', Qt.MatchRecursive)
98+
self.assertEqual(len(items), 1)
99+
items = m.findItems('my_var2', Qt.MatchRecursive)
100+
self.assertEqual(len(items), 1)
101+
items = m.findItems('not_my_var', Qt.MatchRecursive)
102+
self.assertEqual(len(items), 0)
103+
# double check that functions are still only there once
104+
items = m.findItems('lower', Qt.MatchRecursive)
105+
self.assertEqual(len(items), 1)
106+
items = m.findItems('upper', Qt.MatchRecursive)
107+
self.assertEqual(len(items), 1)
108+
109+
def testLayers(self):
110+
""" check that layers are shown in widget model"""
111+
p = QgsProject.instance()
112+
layer = QgsVectorLayer("Point", "layer1", "memory")
113+
layer2 = QgsVectorLayer("Point", "layer2", "memory")
114+
p.addMapLayers([layer, layer2])
115+
116+
w = QgsExpressionBuilderWidget()
117+
m = w.model()
118+
119+
# check that layers are shown
120+
items = m.findItems('layer1', Qt.MatchRecursive)
121+
self.assertEqual(len(items), 1)
122+
items = m.findItems('layer2', Qt.MatchRecursive)
123+
self.assertEqual(len(items), 1)
124+
125+
# change project
126+
p2 = QgsProject()
127+
layer3 = QgsVectorLayer("Point", "layer3", "memory")
128+
p2.addMapLayers([layer3])
129+
w.setProject(p2)
130+
m = w.model()
131+
items = m.findItems('layer1', Qt.MatchRecursive)
132+
self.assertEqual(len(items), 0)
133+
items = m.findItems('layer2', Qt.MatchRecursive)
134+
self.assertEqual(len(items), 0)
135+
items = m.findItems('layer3', Qt.MatchRecursive)
136+
self.assertEqual(len(items), 1)
137+
138+
def testRelations(self):
139+
""" check that layers are shown in widget model"""
140+
p = QgsProject.instance()
141+
142+
# not valid, but doesn't matter for test....
143+
rel = QgsRelation()
144+
rel.setId('rel1')
145+
rel.setName('Relation Number One')
146+
rel.setReferencingLayer(self.referencingLayer.id())
147+
rel.setReferencedLayer(self.referencedLayer.id())
148+
rel.addFieldPair('foreignkey', 'y')
149+
150+
rel2 = QgsRelation()
151+
rel2.setId('rel2')
152+
rel2.setName('Relation Number Two')
153+
rel2.setReferencingLayer(self.referencingLayer.id())
154+
rel2.setReferencedLayer(self.referencedLayer.id())
155+
rel2.addFieldPair('foreignkey', 'y')
156+
157+
p.relationManager().addRelation(rel)
158+
p.relationManager().addRelation(rel2)
159+
160+
w = QgsExpressionBuilderWidget()
161+
m = w.model()
162+
163+
# check that relations are shown
164+
items = m.findItems('Relation Number One', Qt.MatchRecursive)
165+
self.assertEqual(len(items), 1)
166+
items = m.findItems('Relation Number Two', Qt.MatchRecursive)
167+
self.assertEqual(len(items), 1)
168+
169+
170+
if __name__ == '__main__':
171+
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.