Skip to content

Commit dd26e61

Browse files
committed
Add simple local test server convenience classes for unit tests
1 parent 4d03cf7 commit dd26e61

File tree

1 file changed

+320
-0
lines changed

1 file changed

+320
-0
lines changed
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
# -*- coding: utf-8 -*-
2+
"""Convenience interface to a local QGIS Server, e.g. for unit tests
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+
10+
__author__ = 'Larry Shaffer'
11+
__date__ = '07/15/2013'
12+
__copyright__ = 'Copyright 2013, The QGIS Project'
13+
# This will get replaced with a git SHA1 when you do a git archive
14+
__revision__ = '$Format:%H$'
15+
16+
import sys
17+
import os
18+
import ConfigParser
19+
import urllib
20+
import tempfile
21+
22+
# allow import error to be raised if qgis is not on sys.path
23+
try:
24+
from qgis.core import QgsRectangle, QgsCoordinateReferenceSystem
25+
except ImportError, e:
26+
raise ImportError(str(e) + '\n\nPlace path to pyqgis modules on sys.path,'
27+
' or assign to PYTHONPATH')
28+
29+
30+
class ServerNotAccessibleError(Exception):
31+
32+
def __init__(self, cgiurl):
33+
self.msg = """
34+
#----------------------------------------------------------------#
35+
Local test QGIS Server is not accessible at:
36+
{0}
37+
#----------------------------------------------------------------#
38+
""".format(cgiurl)
39+
40+
def __str__(self):
41+
return self.msg
42+
43+
44+
class QgisLocalServer(object):
45+
46+
def __init__(self, cgiurl, chkcapa=False):
47+
self.cgiurl = cgiurl
48+
self.params = {}
49+
self.active = False
50+
51+
# check capabilities to verify server is accessible
52+
if chkcapa:
53+
params = {
54+
'SERVICE': 'WMS',
55+
'VERSION': '1.3.0',
56+
'REQUEST': 'GetCapabilities'
57+
}
58+
if not self.getCapabilities(params, False)[0]:
59+
raise ServerNotAccessibleError(self.cgiurl)
60+
self.active = True
61+
62+
def activeServer(self):
63+
return self.active
64+
65+
def cgiUrl(self):
66+
return self.cgiurl
67+
68+
def getCapabilities(self, params, browser=False):
69+
if (('REQUEST' in params and params['REQUEST'] != 'GetCapabilities') or
70+
'REQUEST' not in params):
71+
params['REQUEST'] = 'GetCapabilities'
72+
73+
self.params = params
74+
url = self.cgiurl + '?' + self._processParams()
75+
self.params = {}
76+
77+
if browser:
78+
openInBrowserTab(url)
79+
return False, ''
80+
81+
res = urllib.urlopen(url)
82+
xml = res.read()
83+
success = ('perhaps you left off the .qgs extension' in xml or
84+
'WMS_Capabilities' in xml)
85+
return success, xml
86+
87+
def getMap(self, params, browser=False):
88+
assert self.active, 'Server not acessible'
89+
90+
msg = 'Parameters should be passed in as a dict'
91+
assert isinstance(params, dict), msg
92+
93+
if (('REQUEST' in params and params['REQUEST'] != 'GetMap') or
94+
'REQUEST' not in params):
95+
params['REQUEST'] = 'GetMap'
96+
97+
self.params = params
98+
url = self.cgiurl + '?' + self._processParams()
99+
self.params = {}
100+
101+
if browser:
102+
openInBrowserTab(url)
103+
return False, ''
104+
105+
tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
106+
tmp.close()
107+
res = urllib.urlretrieve(url, tmp.name)
108+
filepath = res[0]
109+
success = True
110+
if (res[1].getmaintype() != 'image' or
111+
res[1].getheader('Content-Type') != 'image/png'):
112+
success = False
113+
114+
return success, filepath
115+
116+
def _processParams(self):
117+
# set all keys to uppercase
118+
self.params = dict((k.upper(), v) for k, v in self.params.items())
119+
# convert all convenience objects to compatible strings
120+
self._convertInstances()
121+
# encode params
122+
return urllib.urlencode(self.params, True)
123+
124+
def _convertInstances(self):
125+
if not self.params:
126+
return
127+
if ('LAYERS' in self.params and
128+
isinstance(self.params['LAYERS'], list)):
129+
self.params['LAYERS'] = ','.join(self.params['LAYERS'])
130+
if ('BBOX' in self.params and
131+
isinstance(self.params['BBOX'], QgsRectangle)):
132+
# not needed for QGIS's 1.3.0 server?
133+
# # invert x, y of rect and set precision to 16
134+
# rect = self.params['BBOX']
135+
# bbox = ','.join(map(lambda x: '{0:0.16f}'.format(x),
136+
# [rect.yMinimum(), rect.xMinimum(),
137+
# rect.yMaximum(), rect.xMaximum()]))
138+
self.params['BBOX'] = \
139+
self.params['BBOX'].toString(1).replace(' : ', ',')
140+
141+
if ('CRS' in self.params and
142+
isinstance(self.params['CRS'], QgsCoordinateReferenceSystem)):
143+
self.params['CRS'] = self.params['CRS'].authid()
144+
145+
146+
class ServerConfigNotAccessibleError(Exception):
147+
148+
def __init__(self, err=''):
149+
self.msg = '\n\n' + str(err) + '\n'
150+
self.msg += """
151+
#----------------------------------------------------------------#
152+
Local test QGIS Server is not accessible
153+
Check local server configuration settings in:
154+
155+
/<current user>/.qgis2/qgis_local_server.cfg
156+
157+
Adjust settings under the LocalServer section:
158+
protocol = http (recommended)
159+
host = localhost, domain.tld or IP address
160+
port = 80 or a user-defined port above 1024
161+
fcgipath = path to working qgis_mapserv.fcgi as known by server
162+
sourceurl = DO NOT ADJUST
163+
projdir = path WRITEABLE by this user and READABLE by www server
164+
165+
Sample configuration (default):
166+
sourceurl (built) = http://localhost:80/cgi-bin/qgis_mapserv.fcgi
167+
projdir = /var/www/qgis/test-projects
168+
#----------------------------------------------------------------#
169+
"""
170+
171+
def __str__(self):
172+
return self.msg
173+
174+
175+
class QgisLocalServerConfig(QgisLocalServer):
176+
177+
def __init__(self, cfgdir, chkcapa=False):
178+
msg = 'Server configuration directory required'
179+
assert cfgdir, msg
180+
181+
self.cfgdir = cfgdir
182+
self.cfg = os.path.normpath(os.path.join(self.cfgdir,
183+
'qgis_local_server.cfg'))
184+
if not os.path.exists(self.cfg):
185+
msg = ('Default server configuration file could not be written'
186+
' to {0}'.format(self.cfg))
187+
assert self._writeDefaultServerConfig(), msg
188+
189+
self._checkItemFound('file', self.cfg)
190+
self._checkItemReadable('file', self.cfg)
191+
192+
cgiurl, self.projdir = self._readServerConfig()
193+
194+
try:
195+
self._checkItemFound('project directory', self.projdir)
196+
self._checkItemReadable('project directory', self.projdir)
197+
self._checkItemWriteable('project directory', self.projdir)
198+
super(QgisLocalServerConfig, self).__init__(cgiurl, chkcapa)
199+
except Exception, err:
200+
raise ServerConfigNotAccessibleError(err)
201+
202+
def projectDir(self):
203+
return self.projdir
204+
205+
def getMap(self, params, browser=False):
206+
msg = ('Map request parameters should be passed in as a dict '
207+
'(key case can be mixed)')
208+
assert isinstance(params, dict), msg
209+
210+
params = dict((k.upper(), v) for k, v in params.items())
211+
try:
212+
proj = params['MAP']
213+
except KeyError, e:
214+
raise KeyError(str(e) + '\nMAP not found in parameters dict')
215+
216+
if not os.path.exists(proj):
217+
proj = os.path.join(self.projdir, proj)
218+
msg = 'Project could not be found at {0}'.format(proj)
219+
assert os.path.exists(proj), msg
220+
221+
return super(QgisLocalServerConfig, self).getMap(params, browser)
222+
223+
def _checkItemFound(self, item, path):
224+
msg = ('Server configuration {0} could not be found at:\n'
225+
' {1}'.format(item, path))
226+
assert os.path.exists(path), msg
227+
228+
def _checkItemReadable(self, item, path):
229+
msg = ('Server configuration {0} is not readable from:\n'
230+
' {1}'.format(item, path))
231+
assert os.access(path, os.R_OK), msg
232+
233+
def _checkItemWriteable(self, item, path):
234+
msg = ('Server configuration {0} is not writeable from:\n'
235+
' {1}'.format(item, path))
236+
assert os.access(path, os.W_OK), msg
237+
238+
def _writeDefaultServerConfig(self):
239+
"""Overwrites any existing server configuration file with default"""
240+
# http://hub.qgis.org/projects/quantum-gis/wiki/QGIS_Server_Tutorial
241+
# linux: http://localhost/cgi-bin/qgis_mapserv.fcgi? <-- default
242+
# mac: http://localhost/qgis-mapserv/qgis_mapserv.fcgi?
243+
# win: http://localhost/qgis/qgis_mapserv.fcgi?
244+
config = ConfigParser.SafeConfigParser(
245+
{
246+
'sourceurl': 'http://localhost:80/cgi-bin/qgis_mapserv.fcgi',
247+
'projdir': '/var/www/qgis/test-projects'
248+
}
249+
)
250+
config.add_section('LocalServer')
251+
config.set('LocalServer', 'protocol', 'http')
252+
config.set('LocalServer', 'host', 'localhost')
253+
config.set('LocalServer', 'port', '80')
254+
config.set('LocalServer', 'fcgipath', '/cgi-bin/qgis_mapserv.fcgi')
255+
config.set('LocalServer', 'sourceurl',
256+
'%(protocol)s://%(host)s:%(port)s%(fcgipath)s')
257+
config.set('LocalServer', 'projdir', '/var/www/qgis/test-projects')
258+
259+
with open(self.cfg, 'w+') as configfile:
260+
config.write(configfile)
261+
return os.path.exists(self.cfg)
262+
263+
def _readServerConfig(self):
264+
config = ConfigParser.SafeConfigParser()
265+
config.read(self.cfg)
266+
url = config.get('LocalServer', 'sourceurl')
267+
projdir = config.get('LocalServer', 'projdir')
268+
return url, projdir
269+
270+
271+
def openInBrowserTab(url):
272+
if sys.platform[:3] in ('win', 'dar'):
273+
import webbrowser
274+
webbrowser.open_new_tab(url)
275+
else:
276+
# some Linux OS pause execution on webbrowser open, so background it
277+
import subprocess
278+
cmd = 'import webbrowser;' \
279+
'webbrowser.open_new_tab({0})'.format(url)
280+
p = subprocess.Popen([sys.executable, "-c", cmd],
281+
stdout=subprocess.PIPE,
282+
stderr=subprocess.STDOUT).pid
283+
284+
285+
if __name__ == '__main__':
286+
qgishome = os.path.join(os.path.expanduser('~'), '.qgis2')
287+
server = QgisLocalServerConfig(qgishome, True)
288+
# print '\nServer accessible and returned capabilities'
289+
290+
# creating crs needs app instance to access /resources/srs.db
291+
# crs = QgsCoordinateReferenceSystem()
292+
# # default for labeling test data sources: WGS 84 / UTM zone 13N
293+
# crs.createFromSrid(32613)
294+
ext = QgsRectangle(606510, 4823130, 612510, 4827130)
295+
params = {
296+
'SERVICE': 'WMS',
297+
'VERSION': '1.3.0',
298+
'REQUEST': 'GetMap',
299+
'MAP': '/test-projects/tests/tests.qgs',
300+
# layer stacking order for rendering: bottom,to,top
301+
'LAYERS': ['background', 'point'], # or 'background,point'
302+
'STYLES': ',',
303+
'CRS': 'EPSG:32613', # or: QgsCoordinateReferenceSystem obj
304+
'BBOX': ext, # or: '606510,4823130,612510,4827130'
305+
'FORMAT': 'image/png', # or: 'image/png; mode=8bit'
306+
'WIDTH': '600',
307+
'HEIGHT': '400',
308+
'DPI': '72',
309+
'MAP_RESOLUTION': '72',
310+
'FORMAT_OPTIONS': 'dpi:72',
311+
'TRANSPARENT': 'TRUE',
312+
'IgnoreGetMapUrl': '1'
313+
}
314+
if 'QGISSERVER_PNG' in os.environ:
315+
# open resultant png with system
316+
res, filepath = server.getMap(params, False)
317+
openInBrowserTab('file://' + filepath)
318+
else:
319+
# open GetMap url in browser
320+
res, filepath = server.getMap(params, True)

0 commit comments

Comments
 (0)