Skip to content

Commit dc334ac

Browse files
authoredOct 25, 2018
Merge pull request #8223 from signedav/json_tests
QgsServer Tests for JSON fields
2 parents b0104a2 + 0a2689d commit dc334ac

File tree

6 files changed

+395
-13
lines changed

6 files changed

+395
-13
lines changed
 

‎tests/src/python/test_qgsserver_wms.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,18 @@ class TestQgsServerWMSTestBase(QgsServerTestBase):
4848
# Set to True to re-generate reference files for this class
4949
regenerate_reference = False
5050

51-
def wms_request_compare(self, request, extra=None, reference_file=None, project='test_project.qgs', version='1.3.0'):
51+
def wms_request(self, request, extra=None, project='test_project.qgs', version='1.3.0'):
5252
project = self.testdata_path + project
5353
assert os.path.exists(project), "Project file not found: " + project
54-
5554
query_string = 'https://www.qgis.org/?MAP=%s&SERVICE=WMS&VERSION=%s&REQUEST=%s' % (urllib.parse.quote(project), version, request)
5655
if extra is not None:
5756
query_string += extra
5857
header, body = self._execute_request(query_string)
59-
response = header + body
58+
return (header, body, query_string)
59+
60+
def wms_request_compare(self, request, extra=None, reference_file=None, project='test_project.qgs', version='1.3.0'):
61+
response_header, response_body, query_string = self.wms_request(request, extra, project, version)
62+
response = response_header + response_body
6063
reference_path = self.testdata_path + (request.lower() if not reference_file else reference_file) + '.txt'
6164
self.store_reference(reference_path, response)
6265
f = open(reference_path, 'rb')

‎tests/src/python/test_qgsserver_wms_getfeatureinfo.py

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@
1818

1919
import os
2020

21-
# Needed on Qt 5 so that the serialization of XML is consistent among all executions
21+
# Needed on Qt 5 so that the serialization of XML is consistent among all
22+
# executions
2223
os.environ['QT_HASH_SEED'] = '1'
2324

2425
import re
2526
import urllib.request
2627
import urllib.parse
2728
import urllib.error
2829

30+
import xml.etree.ElementTree as ET
31+
import json
32+
2933
from qgis.testing import unittest
3034
from qgis.PyQt.QtCore import QSize
3135

@@ -194,7 +198,8 @@ def testGetFeatureInfo(self):
194198
'wms_getfeatureinfo_notvisible',
195199
'test_project_scalevisibility.qgs')
196200

197-
# Test GetFeatureInfo resolves "value map" widget values but also Server usage of qgs and gpkg file
201+
# Test GetFeatureInfo resolves "value map" widget values but also
202+
# Server usage of qgs and gpkg file
198203
mypath = self.testdata_path + "test_project_values.qgz"
199204
self.wms_request_compare('GetFeatureInfo',
200205
'&layers=layer0&styles=&' +
@@ -240,7 +245,8 @@ def testGetFeatureInfoValueRelationArray(self):
240245
'wms_getfeatureinfo-values3-text-xml',
241246
'test_project_values.qgz')
242247

243-
# TODO make GetFeatureInfo show what's in the display expression and enable test
248+
# TODO make GetFeatureInfo show what's in the display expression and
249+
# enable test
244250
@unittest.expectedFailure
245251
def testGetFeatureInfoRelationReference(self):
246252
"""Test GetFeatureInfo solves "relation reference" widget "display expression" values"""
@@ -267,7 +273,8 @@ def testGetFeatureInfoFilterGPKG(self):
267273
'INFO_FORMAT=text%2Fxml&' +
268274
'width=600&height=400&srs=EPSG%3A3857&' +
269275
'query_layers=testlayer%20%C3%A8%C3%A9&' +
270-
'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.parse.quote(':"NAME" = \'two\''),
276+
'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' +
277+
urllib.parse.quote(':"NAME" = \'two\''),
271278
'wms_getfeatureinfo_filter_gpkg',
272279
'test_project.qgz')
273280

@@ -281,7 +288,8 @@ def testGetFeatureInfoFilter(self):
281288
'INFO_FORMAT=text%2Fxml&' +
282289
'width=600&height=400&srs=EPSG%3A3857&' +
283290
'query_layers=testlayer%20%C3%A8%C3%A9&' +
284-
'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.parse.quote(':"NAME" = \'two\''),
291+
'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' +
292+
urllib.parse.quote(':"NAME" = \'two\''),
285293
'wms_getfeatureinfo_filter')
286294

287295
# Test a filter with NO condition results
@@ -290,7 +298,9 @@ def testGetFeatureInfoFilter(self):
290298
'INFO_FORMAT=text%2Fxml&' +
291299
'width=600&height=400&srs=EPSG%3A3857&' +
292300
'query_layers=testlayer%20%C3%A8%C3%A9&' +
293-
'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.parse.quote(':"NAME" = \'two\' AND "utf8nameè" = \'no-results\''),
301+
'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' +
302+
urllib.parse.quote(
303+
':"NAME" = \'two\' AND "utf8nameè" = \'no-results\''),
294304
'wms_getfeatureinfo_filter_no_results')
295305

296306
# Test a filter with OR condition results
@@ -299,7 +309,9 @@ def testGetFeatureInfoFilter(self):
299309
'INFO_FORMAT=text%2Fxml&' +
300310
'width=600&height=400&srs=EPSG%3A3857&' +
301311
'query_layers=testlayer%20%C3%A8%C3%A9&' +
302-
'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.parse.quote(':"NAME" = \'two\' OR "NAME" = \'three\''),
312+
'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' +
313+
urllib.parse.quote(
314+
':"NAME" = \'two\' OR "NAME" = \'three\''),
303315
'wms_getfeatureinfo_filter_or')
304316

305317
# Test a filter with OR condition and UTF results
@@ -310,16 +322,20 @@ def testGetFeatureInfoFilter(self):
310322
'INFO_FORMAT=text%2Fxml&' +
311323
'width=600&height=400&srs=EPSG%3A3857&' +
312324
'query_layers=testlayer%20%C3%A8%C3%A9&' +
313-
'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.parse.quote(':"NAME" = \'two\' OR "utf8nameè" = \'three èé↓\''),
325+
'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' +
326+
urllib.parse.quote(
327+
':"NAME" = \'two\' OR "utf8nameè" = \'three èé↓\''),
314328
'wms_getfeatureinfo_filter_or_utf8')
315329

316-
# Regression #18292 Server GetFeatureInfo FILTER search fails when WIDTH, HEIGHT are not specified
330+
# Regression #18292 Server GetFeatureInfo FILTER search fails when
331+
# WIDTH, HEIGHT are not specified
317332
self.wms_request_compare('GetFeatureInfo',
318333
'&layers=testlayer%20%C3%A8%C3%A9&' +
319334
'INFO_FORMAT=text%2Fxml&' +
320335
'srs=EPSG%3A3857&' +
321336
'query_layers=testlayer%20%C3%A8%C3%A9&' +
322-
'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' + urllib.parse.quote(':"NAME" = \'two\''),
337+
'FEATURE_COUNT=10&FILTER=testlayer%20%C3%A8%C3%A9' +
338+
urllib.parse.quote(':"NAME" = \'two\''),
323339
'wms_getfeatureinfo_filter_no_width')
324340

325341
def testGetFeatureInfoTolerance(self):
@@ -407,6 +423,89 @@ def testGetFeatureInfoTolerance(self):
407423
'wms_getfeatureinfo_polygon_tolerance_20_text_xml',
408424
'test_project_values.qgz')
409425

426+
def testGetFeatureInfoPostgresTypes(self):
427+
# compare json list output with file
428+
self.wms_request_compare('GetFeatureInfo',
429+
'&layers=json' +
430+
'&info_format=text%2Fxml' +
431+
'&srs=EPSG%3A3857' +
432+
'&QUERY_LAYERS=json' +
433+
'&FILTER=json' +
434+
urllib.parse.quote(':"pk" = 1'),
435+
'get_postgres_types_json_list',
436+
'test_project_postgres_types.qgs')
437+
438+
# compare dict output with file
439+
self.wms_request_compare('GetFeatureInfo',
440+
'&layers=json' +
441+
'&info_format=text%2Fxml' +
442+
'&srs=EPSG%3A3857' +
443+
'&QUERY_LAYERS=json' +
444+
'&FILTER=json' +
445+
urllib.parse.quote(':"pk" = 2'),
446+
'get_postgres_types_json_dict',
447+
'test_project_postgres_types.qgs')
448+
449+
# compare decoded json field list
450+
response_header, response_body, query_string = self.wms_request('GetFeatureInfo',
451+
'&layers=json' +
452+
'&info_format=text%2Fxml' +
453+
'&srs=EPSG%3A3857' +
454+
'&QUERY_LAYERS=json' +
455+
'&FILTER=json' +
456+
urllib.parse.quote(
457+
':"pk" = 1'),
458+
'test_project_postgres_types.qgs')
459+
root = ET.fromstring(response_body)
460+
for attribute in root.iter('Attribute'):
461+
if attribute.get('name') == 'jvalue':
462+
self.assertIsInstance(json.loads(attribute.get('value')), list)
463+
self.assertEqual(json.loads(attribute.get('value')), [1, 2, 3])
464+
self.assertEqual(
465+
json.loads(
466+
attribute.get('value')), [
467+
1.0, 2.0, 3.0])
468+
if attribute.get('name') == 'jbvalue':
469+
self.assertIsInstance(json.loads(attribute.get('value')), list)
470+
self.assertEqual(json.loads(attribute.get('value')), [4, 5, 6])
471+
self.assertEqual(
472+
json.loads(
473+
attribute.get('value')), [
474+
4.0, 5.0, 6.0])
475+
476+
# compare decoded json field dict
477+
response_header, response_body, query_string = self.wms_request('GetFeatureInfo',
478+
'&layers=json' +
479+
'&info_format=text%2Fxml' +
480+
'&srs=EPSG%3A3857' +
481+
'&QUERY_LAYERS=json' +
482+
'&FILTER=json' +
483+
urllib.parse.quote(
484+
':"pk" = 2'),
485+
'test_project_postgres_types.qgs')
486+
root = ET.fromstring(response_body)
487+
for attribute in root.iter('Attribute'):
488+
if attribute.get('name') == 'jvalue':
489+
self.assertIsInstance(json.loads(attribute.get('value')), dict)
490+
self.assertEqual(
491+
json.loads(
492+
attribute.get('value')), {
493+
'a': 1, 'b': 2})
494+
self.assertEqual(
495+
json.loads(
496+
attribute.get('value')), {
497+
'a': 1.0, 'b': 2.0})
498+
if attribute.get('name') == 'jbvalue':
499+
self.assertIsInstance(json.loads(attribute.get('value')), dict)
500+
self.assertEqual(
501+
json.loads(
502+
attribute.get('value')), {
503+
'c': 4, 'd': 5})
504+
self.assertEqual(
505+
json.loads(
506+
attribute.get('value')), {
507+
'c': 4.0, 'd': 5.0})
508+
410509

411510
if __name__ == '__main__':
412511
unittest.main()

‎tests/testdata/qgis_server/db.gpkg

0 Bytes
Binary file not shown.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
*****
2+
Content-Type: text/xml; charset=utf-8
3+
4+
<GetFeatureInfoResponse>
5+
<BoundingBox maxy="0" maxx="0" miny="0" CRS="EPSG:3857" minx="0"/>
6+
<Layer name="json">
7+
<Feature id="2">
8+
<Attribute value="2" name="pk"/>
9+
<Attribute value="{&#xa; &quot;a&quot;: 1,&#xa; &quot;b&quot;: 2&#xa;}&#xa;" name="jvalue"/>
10+
<Attribute value="{&#xa; &quot;c&quot;: 4,&#xa; &quot;d&quot;: 5&#xa;}&#xa;" name="jbvalue"/>
11+
</Feature>
12+
</Layer>
13+
</GetFeatureInfoResponse>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
*****
2+
Content-Type: text/xml; charset=utf-8
3+
4+
<GetFeatureInfoResponse>
5+
<BoundingBox maxy="0" maxx="0" miny="0" CRS="EPSG:3857" minx="0"/>
6+
<Layer name="json">
7+
<Feature id="1">
8+
<Attribute value="1" name="pk"/>
9+
<Attribute value="[&#xa; 1,&#xa; 2,&#xa; 3&#xa;]&#xa;" name="jvalue"/>
10+
<Attribute value="[&#xa; 4,&#xa; 5,&#xa; 6&#xa;]&#xa;" name="jbvalue"/>
11+
</Feature>
12+
</Layer>
13+
</GetFeatureInfoResponse>
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
2+
<qgis projectname="" version="3.3.0-Master">
3+
<homePath path=""/>
4+
<title></title>
5+
<autotransaction active="0"/>
6+
<evaluateDefaultValues active="0"/>
7+
<trust active="0"/>
8+
<projectCrs>
9+
<spatialrefsys>
10+
<proj4>+proj=longlat +datum=WGS84 +no_defs</proj4>
11+
<srsid>3452</srsid>
12+
<srid>4326</srid>
13+
<authid>EPSG:4326</authid>
14+
<description>WGS 84</description>
15+
<projectionacronym>longlat</projectionacronym>
16+
<ellipsoidacronym>WGS84</ellipsoidacronym>
17+
<geographicflag>true</geographicflag>
18+
</spatialrefsys>
19+
</projectCrs>
20+
<layer-tree-group>
21+
<customproperties/>
22+
<layer-tree-layer expanded="1" checked="Qt::Checked" source="dbname='qgis_test' port=5432 user='postgres' sslmode=disable key='pk' table=&quot;qgis_test&quot;.&quot;json&quot; sql=" providerKey="postgres" id="json_b2af056d_ba24_4fb7_805b_5a7720e242f3" name="json">
23+
<customproperties/>
24+
</layer-tree-layer>
25+
<custom-order enabled="0"/>
26+
</layer-tree-group>
27+
<snapping-settings enabled="0" type="1" intersection-snapping="0" unit="2" tolerance="0" mode="2">
28+
<individual-layer-settings/>
29+
</snapping-settings>
30+
<relations/>
31+
<mapcanvas annotationsVisible="1" name="theMapCanvas">
32+
<units>degrees</units>
33+
<extent>
34+
<xmin>8.20313020806923632</xmin>
35+
<ymin>44.90118727027074641</ymin>
36+
<xmax>8.20717330488028196</xmax>
37+
<ymax>44.90224039332306205</ymax>
38+
</extent>
39+
<rotation>0</rotation>
40+
<destinationsrs>
41+
<spatialrefsys>
42+
<proj4>+proj=longlat +datum=WGS84 +no_defs</proj4>
43+
<srsid>3452</srsid>
44+
<srid>4326</srid>
45+
<authid>EPSG:4326</authid>
46+
<description>WGS 84</description>
47+
<projectionacronym>longlat</projectionacronym>
48+
<ellipsoidacronym>WGS84</ellipsoidacronym>
49+
<geographicflag>true</geographicflag>
50+
</spatialrefsys>
51+
</destinationsrs>
52+
<rendermaptile>0</rendermaptile>
53+
</mapcanvas>
54+
<projectModels/>
55+
<legend updateDrawingOrder="true">
56+
<legendlayer showFeatureCount="0" drawingOrder="-1" open="true" checked="Qt::Checked" name="json">
57+
<filegroup open="true" hidden="false">
58+
<legendlayerfile visible="1" layerid="json_b2af056d_ba24_4fb7_805b_5a7720e242f3" isInOverview="0"/>
59+
</filegroup>
60+
</legendlayer>
61+
</legend>
62+
<mapViewDocks/>
63+
<projectlayers>
64+
<maplayer maxScale="0" autoRefreshEnabled="0" hasScaleBasedVisibilityFlag="0" type="vector" refreshOnNotifyMessage="" autoRefreshTime="0" geometry="No geometry" styleCategories="AllStyleCategories" refreshOnNotifyEnabled="0" minScale="1e+8" readOnly="0">
65+
<id>json_b2af056d_ba24_4fb7_805b_5a7720e242f3</id>
66+
<datasource>dbname='qgis_test' port=5432 sslmode=disable key='pk' table="qgis_test"."json" sql=</datasource>
67+
<keywordList>
68+
<value></value>
69+
</keywordList>
70+
<layername>json</layername>
71+
<srs>
72+
<spatialrefsys>
73+
<proj4></proj4>
74+
<srsid>0</srsid>
75+
<srid>0</srid>
76+
<authid></authid>
77+
<description></description>
78+
<projectionacronym></projectionacronym>
79+
<ellipsoidacronym></ellipsoidacronym>
80+
<geographicflag>true</geographicflag>
81+
</spatialrefsys>
82+
</srs>
83+
<resourceMetadata>
84+
<identifier></identifier>
85+
<parentidentifier></parentidentifier>
86+
<language></language>
87+
<type>dataset</type>
88+
<title></title>
89+
<abstract></abstract>
90+
<links/>
91+
<fees></fees>
92+
<encoding></encoding>
93+
<crs>
94+
<spatialrefsys>
95+
<proj4></proj4>
96+
<srsid>0</srsid>
97+
<srid>0</srid>
98+
<authid></authid>
99+
<description></description>
100+
<projectionacronym></projectionacronym>
101+
<ellipsoidacronym></ellipsoidacronym>
102+
<geographicflag>false</geographicflag>
103+
</spatialrefsys>
104+
</crs>
105+
<extent/>
106+
</resourceMetadata>
107+
<provider encoding="ISO-8859-5">postgres</provider>
108+
<vectorjoins/>
109+
<layerDependencies/>
110+
<dataDependencies/>
111+
<legend type="default-vector"/>
112+
<expressionfields/>
113+
<map-layer-style-manager current="default">
114+
<map-layer-style name="default"/>
115+
</map-layer-style-manager>
116+
<auxiliaryLayer/>
117+
<flags>
118+
<Identifiable>1</Identifiable>
119+
<Removable>1</Removable>
120+
<Searchable>1</Searchable>
121+
</flags>
122+
<customproperties>
123+
<property key="dualview/previewExpressions" value="&quot;pk&quot;"/>
124+
</customproperties>
125+
<geometryOptions removeDuplicateNodes="0" geometryPrecision="0"/>
126+
<fieldConfiguration>
127+
<field name="pk">
128+
<editWidget type="">
129+
<config>
130+
<Option/>
131+
</config>
132+
</editWidget>
133+
</field>
134+
<field name="jvalue">
135+
<editWidget type="">
136+
<config>
137+
<Option/>
138+
</config>
139+
</editWidget>
140+
</field>
141+
<field name="jbvalue">
142+
<editWidget type="">
143+
<config>
144+
<Option/>
145+
</config>
146+
</editWidget>
147+
</field>
148+
</fieldConfiguration>
149+
<aliases>
150+
<alias field="pk" index="0" name=""/>
151+
<alias field="jvalue" index="1" name=""/>
152+
<alias field="jbvalue" index="2" name=""/>
153+
</aliases>
154+
<excludeAttributesWMS/>
155+
<excludeAttributesWFS/>
156+
<defaults>
157+
<default field="pk" applyOnUpdate="0" expression=""/>
158+
<default field="jvalue" applyOnUpdate="0" expression=""/>
159+
<default field="jbvalue" applyOnUpdate="0" expression=""/>
160+
</defaults>
161+
<constraints>
162+
<constraint constraints="3" field="pk" notnull_strength="1" exp_strength="0" unique_strength="1"/>
163+
<constraint constraints="0" field="jvalue" notnull_strength="0" exp_strength="0" unique_strength="0"/>
164+
<constraint constraints="0" field="jbvalue" notnull_strength="0" exp_strength="0" unique_strength="0"/>
165+
</constraints>
166+
<constraintExpressions>
167+
<constraint field="pk" exp="" desc=""/>
168+
<constraint field="jvalue" exp="" desc=""/>
169+
<constraint field="jbvalue" exp="" desc=""/>
170+
</constraintExpressions>
171+
<expressionfields/>
172+
<attributeactions>
173+
<defaultAction key="Canvas" value="{00000000-0000-0000-0000-000000000000}"/>
174+
</attributeactions>
175+
<attributetableconfig actionWidgetStyle="dropDown" sortOrder="0" sortExpression="">
176+
<columns/>
177+
</attributetableconfig>
178+
<conditionalstyles>
179+
<rowstyles/>
180+
<fieldstyles/>
181+
</conditionalstyles>
182+
<editform tolerant="1"></editform>
183+
<editforminit/>
184+
<editforminitcodesource>0</editforminitcodesource>
185+
<editforminitfilepath></editforminitfilepath>
186+
<editforminitcode><![CDATA[]]></editforminitcode>
187+
<featformsuppress>0</featformsuppress>
188+
<editorlayout>generatedlayout</editorlayout>
189+
<editable/>
190+
<labelOnTop/>
191+
<widgets/>
192+
<previewExpression>"pk"</previewExpression>
193+
<mapTip></mapTip>
194+
</maplayer>
195+
</projectlayers>
196+
<layerorder/>
197+
<properties>
198+
<Measure>
199+
<Ellipsoid type="QString">WGS84</Ellipsoid>
200+
</Measure>
201+
<SpatialRefSys>
202+
<ProjectionsEnabled type="int">1</ProjectionsEnabled>
203+
</SpatialRefSys>
204+
<Gui>
205+
<CanvasColorBluePart type="int">255</CanvasColorBluePart>
206+
<CanvasColorGreenPart type="int">255</CanvasColorGreenPart>
207+
<SelectionColorGreenPart type="int">255</SelectionColorGreenPart>
208+
<SelectionColorBluePart type="int">0</SelectionColorBluePart>
209+
<CanvasColorRedPart type="int">255</CanvasColorRedPart>
210+
<SelectionColorAlphaPart type="int">255</SelectionColorAlphaPart>
211+
<SelectionColorRedPart type="int">255</SelectionColorRedPart>
212+
</Gui>
213+
<PositionPrecision>
214+
<DecimalPlaces type="int">2</DecimalPlaces>
215+
<Automatic type="bool">true</Automatic>
216+
</PositionPrecision>
217+
<Legend>
218+
<filterByMap type="bool">false</filterByMap>
219+
</Legend>
220+
<PAL>
221+
<CandidatesPoint type="int">16</CandidatesPoint>
222+
<CandidatesLine type="int">50</CandidatesLine>
223+
<ShowingAllLabels type="bool">false</ShowingAllLabels>
224+
<SearchMethod type="int">0</SearchMethod>
225+
<DrawRectOnly type="bool">false</DrawRectOnly>
226+
<CandidatesPolygon type="int">30</CandidatesPolygon>
227+
<ShowingCandidates type="bool">false</ShowingCandidates>
228+
<ShowingPartialsLabels type="bool">true</ShowingPartialsLabels>
229+
<DrawOutlineLabels type="bool">true</DrawOutlineLabels>
230+
</PAL>
231+
<Paths>
232+
<Absolute type="bool">false</Absolute>
233+
</Paths>
234+
<Measurement>
235+
<DistanceUnits type="QString">meters</DistanceUnits>
236+
<AreaUnits type="QString">m2</AreaUnits>
237+
</Measurement>
238+
</properties>
239+
<visibility-presets/>
240+
<transformContext/>
241+
<projectMetadata>
242+
<identifier></identifier>
243+
<parentidentifier></parentidentifier>
244+
<language></language>
245+
<type></type>
246+
<title></title>
247+
<abstract></abstract>
248+
<links/>
249+
<author>David</author>
250+
<creation>2018-10-17T15:28:25</creation>
251+
</projectMetadata>
252+
<Annotations/>
253+
<Layouts/>
254+
</qgis>

0 commit comments

Comments
 (0)
Please sign in to comment.