Plugin-uri Python pentru Serverul QGIS¶
Atenționare
Despite our constant efforts, information beyond this line may not be updated for QGIS 3. Refer to https://qgis.org/pyqgis/master for the python API documentation or, give a hand to update the chapters you know about. Thanks.
Python plugins can also run on QGIS Server (see QGIS ca și Server de Date OGC):
By using the server interface (
QgsServerInterface
) a Python plugin running on the server can alter the behavior of existing core services (WMS, WFS etc.).With the server filter interface (
QgsServerFilter
) you can change the input parameters, change the generated output or even provide new services.With the access control interface (
QgsAccessControlFilter
) you can apply some access restriction per requests.
Arhitectura Plugin-urilor de Filtrare de pe Server¶
Server python plugins are loaded once when the FCGI application starts. They
register one or more QgsServerFilter
(from this point, you might find useful a quick look to the server plugins
API docs). Each filter should implement at least one of
three callbacks:
All filters have access to the request/response object (QgsRequestHandler
)
and can manipulate all its properties (input/output) and
raise exceptions (while in a quite particular way as we’ll see below).
Mai jos se află un pseudocod care prezintă o sesiune tipică de server și reapelarea filtrelor:
- se obține cererea de intrare
se creează o rutină de tratare a cererilor GET/POST/SOAP
pass request to an instance of
QgsServerInterface
call plugins
requestReady
filters- în cazul în care nu există un răspuns
- dacă SERVICE este de tipul WMS/WFS/WCS
- se creează serverul WMS/WFS/WCS
call server’s
executeRequest
and possibly callsendResponse
plugin filters when streaming output or store the byte stream output and content type in the request handler
call plugins
responseComplete
filters
call plugins
sendResponse
filtersrequest handler output the response
Următoarele paragrafe descriu, în detaliu, funcțiile de reapelare disponibile.
requestReady¶
Este apelată atunci când cererea este pregătită: adresa și datele primite au fost analizate și, înainte de a intra în comutatorul serviciilor de bază (WMS, WFS, etc), acesta este punctul în care se poate interveni asupra datelor de intrare, putându-se efectua acțiuni de genul:
autentificare/autorizare
redirectări
adăugarea/eliminarea anumitor parametri (denumirile tipurilor, de exemplu)
tratarea excepțiilor
Ați putea chiar să substituiți în întregime un serviciu de bază, prin schimbarea parametrului SERVICE, astfel, ocolindu-se complet serviciul de bază (deși, acest lucru nu ar avea prea mult sens).
sendResponse¶
This is called whenever output is sent to FCGI stdout
(and from there, to
the client), this is normally done after core services have finished their process
and after responseComplete hook was called, but in a few cases XML can become so
huge that a streaming XML implementation was needed (WFS GetFeature is one of them),
in this case, sendResponse
is
called multiple times before the response
is complete (and before
responseComplete
is called).
The obvious consequence
is that sendResponse
is
normally called once but might be exceptionally
called multiple times and in that case (and only in that case) it is also called
before responseComplete
.
sendResponse
is the best place
for direct manipulation of core service’s
output and while responseComplete
is typically also an option,
sendResponse
is the only
viable option in case of streaming services.
responseComplete¶
This is called once when core services (if hit) finish their process and the
request is ready to be sent to the client. As discussed above, this is normally
called before sendResponse
except for streaming services (or other plugin
filters) that might have called
sendResponse
earlier.
responseComplete
is the
ideal place to provide new services implementation
(WPS or custom services) and to perform direct manipulation of the output coming
from core services (for example to add a watermark upon a WMS image).
Tratarea excepțiilor provenite de la un plugin¶
Some work has still to be done on this topic: the current implementation can
distinguish between handled and unhandled exceptions by setting a
QgsRequestHandler
property to an
instance of QgsMapServiceException
,
this way the main C++ code can catch handled python exceptions and ignore
unhandled exceptions (or better: log them).
Această abordare funcționează în principiu, dar nu este în spiritul limbajului „python”: o abordare mai bună ar fi de a face vizibile excepțiile din codul python la nivelul buclei C++, pentru a fi manipulată acolo.
Scrierea unui plugin pentru server¶
A server plugin is a standard QGIS Python plugin as described in
Developing Python Plugins, that just provides an additional (or alternative)
interface: a typical QGIS desktop plugin has access to QGIS application
through the QgisInterface
instance, a server
plugin has also
access to a QgsServerInterface
.
Pentru a spune Serverului QGIS că un plugin are o interfață de server, este necesară o intrare de metadate specială (în metadata.txt)
server=True
The example plugin discussed here (with many more example filters) is available on github: QGIS HelloServer Example Plugin. You could also find more examples at https://github.com/elpaso/qgis3-server-vagrant/tree/master/resources/web/plugins or browsing the QGIS plugins repository.
Fișierele Plugin-ului¶
Iată structura de directoare a exemplului nostru de plugin pentru server
PYTHON_PLUGINS_PATH/
HelloServer/
__init__.py --> *required*
HelloServer.py --> *required*
metadata.txt --> *required*
__init__.py¶
This file is required by Python’s import system. Also, QGIS Server requires that this
file contains a serverClassFactory()
function, which is called when the
plugin gets loaded into QGIS Server when the server starts. It receives reference to instance of
QgsServerInterface
and must return instance
of your plugin’s class.
This is how the example plugin __init__.py
looks like:
# -*- coding: utf-8 -*-
def serverClassFactory(serverIface):
from HelloServer import HelloServerServer
return HelloServerServer(serverIface)
HelloServer.py¶
Aici este locul în care se întâmplă magia, și iată rezultatul acesteia: (de exemplu HelloServer.py
)
Un plug-in de server este format, de obicei, dintr-una sau mai multe funcții Callback, ambalate în obiecte denumite QgsServerFilter.
Each QgsServerFilter
implements one or more
of the following callbacks:
Exemplul următor implementează un filtru minimal, care generează textul HelloServer! atunci când parametrul SERVICE este egal cu “HELLO”:
from qgis.server import *
from qgis.core import *
class HelloFilter(QgsServerFilter):
def __init__(self, serverIface):
super(HelloFilter, self).__init__(serverIface)
def responseComplete(self):
request = self.serverInterface().requestHandler()
params = request.parameterMap()
if params.get('SERVICE', '').upper() == 'HELLO':
request.clearHeaders()
request.setHeader('Content-type', 'text/plain')
request.clearBody()
request.appendBody('HelloServer!')
Filtrele trebuie să fie înregistrate în serverIface ca în exemplul următor:
class HelloServerServer:
def __init__(self, serverIface):
# Save reference to the QGIS server interface
self.serverIface = serverIface
serverIface.registerFilter( HelloFilter, 100 )
The second parameter of
registerFilter
sets a priority which
defines the order for the callbacks with the same name (the lower priority is
invoked first).
By using the three callbacks, plugins can manipulate the input and/or the output
of the server in many different ways. In every moment, the plugin instance has
access to the QgsRequestHandler
through
the QgsServerInterface
.
The QgsRequestHandler
class has plenty of
methods that can be used to alter
the input parameters before entering the core processing of the server (by using
requestReady()
) or after the request has been processed by the core services
(by using sendResponse()
).
Următorul exemplu demonstrează câteva cazuri de utilizare obișnuită:
Modificarea intrării¶
The example plugin contains a test example that changes input parameters coming
from the query string, in this example a new parameter is injected into the
(already parsed) parameterMap
, this parameter is then visible by core services
(WMS etc.), at the end of core services processing we check that the parameter
is still there:
from qgis.server import *
from qgis.core import *
class ParamsFilter(QgsServerFilter):
def __init__(self, serverIface):
super(ParamsFilter, self).__init__(serverIface)
def requestReady(self):
request = self.serverInterface().requestHandler()
params = request.parameterMap( )
request.setParameter('TEST_NEW_PARAM', 'ParamsFilter')
def responseComplete(self):
request = self.serverInterface().requestHandler()
params = request.parameterMap( )
if params.get('TEST_NEW_PARAM') == 'ParamsFilter':
QgsMessageLog.logMessage("SUCCESS - ParamsFilter.responseComplete", 'plugin', QgsMessageLog.INFO)
else:
QgsMessageLog.logMessage("FAIL - ParamsFilter.responseComplete", 'plugin', QgsMessageLog.CRITICAL)
This is an extract of what you see in the log file:
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloServerServer - loading filter ParamsFilter
src/core/qgsmessagelog.cpp: 45: (logMessage) [1ms] 2014-12-12T12:39:29 Server[0] Server plugin HelloServer loaded!
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 Server[0] Server python plugins loaded
src/mapserver/qgsgetrequesthandler.cpp: 35: (parseInput) [0ms] query string is: SERVICE=HELLO&request=GetOutput
src/mapserver/qgshttprequesthandler.cpp: 547: (requestStringToParameterMap) [1ms] inserting pair SERVICE // HELLO into the parameter map
src/mapserver/qgshttprequesthandler.cpp: 547: (requestStringToParameterMap) [0ms] inserting pair REQUEST // GetOutput into the parameter map
src/mapserver/qgsserverfilter.cpp: 42: (requestReady) [0ms] QgsServerFilter plugin default requestReady called
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloFilter.requestReady
src/mapserver/qgis_map_serv.cpp: 235: (configPath) [0ms] Using default configuration file path: /home/xxx/apps/bin/admin.sld
src/mapserver/qgshttprequesthandler.cpp: 49: (setHttpResponse) [0ms] Checking byte array is ok to set...
src/mapserver/qgshttprequesthandler.cpp: 59: (setHttpResponse) [0ms] Byte array looks good, setting response...
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloFilter.responseComplete
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] SUCCESS - ParamsFilter.responseComplete
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] RemoteConsoleFilter.responseComplete
src/mapserver/qgshttprequesthandler.cpp: 158: (sendResponse) [0ms] Sending HTTP response
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloFilter.sendResponse
On the highlighted line the “SUCCESS” string indicates that the plugin passed the test.
Aceeași tehnică poate fi exploatată pentru a utiliza un serviciu personalizat in locul unuia de bază: de exemplu, ați putea sări peste o cerere WFS SERVICE, sau peste oricare altă cerere de bază, doar prin schimbarea parametrului SERVICE în ceva diferit, iar serviciul de bază va fi omis; în acel caz, veți puteți injecta datele dvs. în interiorul rezultatului, trimițându-le clientului (acest lucru este explicat în continuare).
Modificarea sau înlocuirea rezultatului¶
The watermark filter example shows how to replace the WMS output with a new image obtained by adding a watermark image on the top of the WMS image generated by the WMS core service:
import os
from qgis.server import *
from qgis.core import *
from qgis.PyQt.QtCore import *
from qgis.PyQt.QtGui import *
class WatermarkFilter(QgsServerFilter):
def __init__(self, serverIface):
super(WatermarkFilter, self).__init__(serverIface)
def responseComplete(self):
request = self.serverInterface().requestHandler()
params = request.parameterMap( )
# Do some checks
if (request.parameter('SERVICE').upper() == 'WMS' \
and request.parameter('REQUEST').upper() == 'GETMAP' \
and not request.exceptionRaised() ):
QgsMessageLog.logMessage("WatermarkFilter.responseComplete: image ready {}".format(request.infoFormat()), 'plugin', QgsMessageLog.INFO)
# Get the image
img = QImage()
img.loadFromData(request.body())
# Adds the watermark
watermark = QImage(os.path.join(os.path.dirname(__file__), 'media/watermark.png'))
p = QPainter(img)
p.drawImage(QRect( 20, 20, 40, 40), watermark)
p.end()
ba = QByteArray()
buffer = QBuffer(ba)
buffer.open(QIODevice.WriteOnly)
img.save(buffer, "PNG")
# Set the body
request.clearBody()
request.appendBody(ba)
În cadrul acestui exemplu, este verificată valoarea parametrului SERVICE, iar în cazul în care cererea de intrare este un WMS GETMAP și nici un fel de excepții nu au fost stabilite de către un plugin executat anterior, sau de către serviciul de bază (WMS în acest caz), imaginea generată de către WMS este preluată din zona tampon de ieșire, adăugându-i-se imaginea filigran. Pasul final este de a goli tamponul de ieșire și de-l înlocui cu imaginea nou generată. Rețineți că într-o situație reală, ar trebui, de asemenea, să verificați tipul imaginii solicitate în loc de a returna, în toate cazurile, PNG-ul.
Plugin-ul de control al accesului¶
Fișierele Plugin-ului¶
Iată structura de directoare a exemplului nostru de plugin pentru server:
PYTHON_PLUGINS_PATH/
MyAccessControl/
__init__.py --> *required*
AccessControl.py --> *required*
metadata.txt --> *required*
__init__.py¶
This file is required by Python’s import system. As for all QGIS server plugins, this
file contains a serverClassFactory()
function, which is called when the
plugin gets loaded into QGIS Server at startup. It receives a reference to an instance of
QgsServerInterface
and must return an instance
of your plugin’s class.
This is how the example plugin __init__.py
looks like:
# -*- coding: utf-8 -*-
def serverClassFactory(serverIface):
from MyAccessControl.AccessControl import AccessControl
return AccessControl(serverIface)
AccessControl.py¶
class AccessControl(QgsAccessControlFilter):
def __init__(self, server_iface):
super(QgsAccessControlFilter, self).__init__(server_iface)
def layerFilterExpression(self, layer):
""" Return an additional expression filter """
return super(QgsAccessControlFilter, self).layerFilterExpression(layer)
def layerFilterSubsetString(self, layer):
""" Return an additional subset string (typically SQL) filter """
return super(QgsAccessControlFilter, self).layerFilterSubsetString(layer)
def layerPermissions(self, layer):
""" Return the layer rights """
return super(QgsAccessControlFilter, self).layerPermissions(layer)
def authorizedLayerAttributes(self, layer, attributes):
""" Return the authorised layer attributes """
return super(QgsAccessControlFilter, self).authorizedLayerAttributes(layer, attributes)
def allowToEdit(self, layer, feature):
""" Are we authorise to modify the following geometry """
return super(QgsAccessControlFilter, self).allowToEdit(layer, feature)
def cacheKey(self):
return super(QgsAccessControlFilter, self).cacheKey()
Acest exemplu oferă un acces deplin pentru oricine.
Este de datoria plugin-ului să știe cine este conectat.
Toate aceste metode au ca argument stratul, pentru a putea personaliza restricțiile pentru fiecare strat.
layerFilterExpression¶
Se folosește pentru a adăuga o Expresie de limitare a rezultatelor, ex.:
def layerFilterExpression(self, layer):
return "$role = 'user'"
Pentru restrângerea la entitățile pentru care atributul „rol” are valoarea „user”.
layerFilterSubsetString¶
La fel ca și precedenta, dar folosește SubsetString
(executată în baza de date)
def layerFilterSubsetString(self, layer):
return "role = 'user'"
Pentru restrângerea la entitățile pentru care atributul „rol” are valoarea „user”.
layerPermissions¶
Limitează accesul la strat.
Return an object of type LayerPermissions
, which has the properties:
canRead
to see it in theGetCapabilities
and have read access.canInsert
to be able to insert a new feature.canUpdate
to be able to update a feature.canDelete
to be able to delete a feature.
Exemplu:
def layerPermissions(self, layer):
rights = QgsAccessControlFilter.LayerPermissions()
rights.canRead = True
rights.canRead = rights.canInsert = rights.canUpdate = rights.canDelete = False
return rights
Pentru a permite tuturor accesul numai pentru citire.
authorizedLayerAttributes¶
Folosit pentru a reduce vizibilitatea unui subset specific de atribute.
Atributul argument returnează setul actual de atribute vizibile.
Exemplu:
def authorizedLayerAttributes(self, layer, attributes):
return [a for a in attributes if a != "role"]
Pentru a ascunde atributul «role».
allowToEdit¶
Se folosește pentru a limita editarea unui subset specific de entități.
Este folosit în protocolul WFS-Transaction
.
Exemplu:
def allowToEdit(self, layer, feature):
return feature.attribute('role') == 'user'
Pentru a putea modifica numai entitatea pentru care atributul „rol” are valoarea de „utilizator”.