def run(self): appdef = None projFile = QgsProject.instance().fileName() if projFile: appdefFile = projFile + ".appdef" if os.path.exists(appdefFile): if pluginSetting("askreload") == "Ask": ret = QMessageBox.question(self.iface.mainWindow(), "Web app builder", "This project has been already published as a web app.\n" "Do you want to reload app configuration?", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if ret == QMessageBox.Yes: appdef = loadAppdef(appdefFile) elif pluginSetting("askreload") == "Open last configuration": appdef = loadAppdef(appdefFile) initialize() # reset credential token in case related credentials are changed utils.resetCachedToken() try: dlg = MainDialog(appdef) dlg.exec_() except: dlg.progressBar.setMaximum(100) dlg.progressBar.setValue(0) dlg.progressBar.setVisible(False) dlg.progressLabel.setVisible(False) QApplication.restoreOverrideCursor() QgsMessageLog.logMessage(traceback.format_exc(), "WebAppBuilder", level=QgsMessageLog.CRITICAL) QMessageBox.critical(self.iface.mainWindow(), "Unmanaged error. See QGIS log for more details.")
def getToken(): """ Function to get a access token from endpoint sending "custom" basic auth. Parameters The return value is a token string or Exception. This is cached and returned every call or request again if cache is empty """ global __cachedToken if __cachedToken: return __cachedToken # start with a clean cache __cachedToken = None # get authcfg to point to saved credentials in QGIS Auth manager authcfg = getConnectAuthCfg() if not authcfg: raise Exception("Connect authcfg is empty") usr, pwd = getCredentialsFromAuthDb(authcfg) if not usr and not pwd: raise Exception( "Cannot find stored credentials with authcfg = {}".format(authcfg)) # prepare data for the token request httpAuth = base64.b64encode('{}:{}'.format(usr.strip(), pwd.strip())).decode("ascii") headers = {} headers["Authorization"] = "Basic {}".format(httpAuth) headers["Content-Type"] = "application/json" # request token in synchronous way => block GUI nam = NetworkAccessManager(debug=pluginSetting("logresponse")) try: res, resText = nam.request(authUrl(), method="GET", headers=headers) except Exception as e: if nam.http_call_result.status_code in [401, 403]: raise Exception( "Permission denied with current Connect credentials") else: raise e # todo: check res code in case not authorization if not res.ok: raise Exception("Cannot get token: {}".format(res.reason)) # parse token from resText resDict = json.loads(str(resText)) try: __cachedToken = resDict["token"] except: pass if not __cachedToken: raise Exception("Cannot get authentication token") return __cachedToken
def appSDKification(folder, progress): ''' zip app folder and send to WAB compiler to apply SDK compilation. The returned zip will be the official webapp ''' progress.oscillate() progress.setText("Get Authorization token") try: global __appSDKification_doAgain if __appSDKification_doAgain: QgsMessageLog.logMessage("Renew token in case of it is expired and retry", level=QgsMessageLog.WARNING) utils.resetCachedToken() token = utils.getToken() except Exception as e: pub.sendMessage(utils.topics.endAppSDKification, success=False, reason=str(e)) return # zip folder to send for compiling progress.setText("Preparing data to compile") zipFileName = tempFilenameInTempFolder("webapp.zip", "webappbuilder") try: with zipfile.ZipFile(zipFileName, "w") as zf: relativeFrom = os.path.dirname(folder) for dirname, subdirs, files in os.walk(folder): # exclude data folder if 'data' in subdirs: subdirs.remove('data') if relativeFrom in dirname: zf.write(dirname, dirname[len(relativeFrom):]) for filename in files: fiename = os.path.join(dirname, filename) zf.write(fiename, fiename[len(relativeFrom):]) except: msg = "Could not zip webapp folder: {}".format(folder) pub.sendMessage(utils.topics.endAppSDKification, success=False, reason=msg) return # prepare data for WAB compiling request with open(zipFileName, 'rb') as f: fileContent = f.read() fields = { 'file': (os.path.basename(zipFileName), fileContent) } payload, content_type = encode_multipart_formdata(fields) headers = {} headers["authorization"] = "Bearer {}".format(token) headers["Content-Type"] = content_type # prepare request (as in NetworkAccessManager) but without blocking request # do http post progress.setText("Wait compilation") global __anam if __anam: del __anam __anam = None __anam = AsyncNetworkAccessManager(debug=pluginSetting("logresponse")) __anam.request(utils.wabCompilerUrl(), method='POST', body=payload, headers=headers, blocking=False) __anam.reply.finished.connect( lambda: manageFinished(__anam, zipFileName, folder, progress) )
def zoomTo(self): apikey = pluginSetting("apiKey") if apikey is None or apikey == "": self._showMessage( 'what3words API key is not set. Please set it and try again.', QgsMessageBar.WARNING) return self.zoomToDialog.setApiKey(apikey) self.zoomToDialog.show()
def getLegendSymbols(self, layer, ilayer, legendFolder): size = self._parameters["size"] qsize = QSize(size, size) symbols = [] def appendSymbol(title, href): symbols.append({'title': title, 'href': href}) if layer.type() == layer.VectorLayer: renderer = layer.rendererV2() if isinstance(renderer, QgsSingleSymbolRendererV2): img = renderer.symbol().asImage(qsize) symbolPath = os.path.join(legendFolder, "%i_0.png" % (ilayer)) img.save(symbolPath) appendSymbol("", os.path.basename(symbolPath)) elif isinstance(renderer, QgsCategorizedSymbolRendererV2): for isymbol, cat in enumerate(renderer.categories()): img = cat.symbol().asImage(qsize) symbolPath = os.path.join(legendFolder, "%i_%i.png" % (ilayer, isymbol)) img.save(symbolPath) appendSymbol(cat.label(), os.path.basename(symbolPath)) elif isinstance(renderer, QgsGraduatedSymbolRendererV2): for isymbol, ran in enumerate(renderer.ranges()): img = ran.symbol().asImage(qsize) symbolPath = os.path.join(legendFolder, "%i_%i.png" % (ilayer, isymbol)) img.save(symbolPath) appendSymbol( "%s-%s" % (ran.lowerValue(), ran.upperValue()), os.path.basename(symbolPath)) elif isinstance(renderer, QgsRuleBasedRendererV2): for isymbol, rule in enumerate(renderer.rootRule().children()): img = rule.symbol().asImage(qsize) symbolPath = os.path.join(legendFolder, "%i_%i.png" % (ilayer, isymbol)) img.save(symbolPath) appendSymbol(rule.label(), os.path.basename(symbolPath)) elif layer.type() == layer.RasterLayer: if layer.providerType() == "wms": source = layer.source() layerName = re.search(r"layers=(.*?)(?:&|$)", source).groups(0)[0] url = re.search(r"url=(.*?)(?:&|$)", source).groups(0)[0] styles = re.search(r"styles=(.*?)(?:&|$)", source).groups(0)[0] fullUrl = ( "%s?LAYER=%s&STYLES=%s&REQUEST=GetLegendGraphic&VERSION=1.0.0&FORMAT=image/png&WIDTH=%i&HEIGHT=%i" % (url, layerName, styles, size, size)) nam = NetworkAccessManager(debug=pluginSetting("logresponse")) response, content = nam.request(fullUrl) symbolPath = os.path.join(legendFolder, "%i_0.png" % ilayer) with open(symbolPath, 'wb') as f: f.write(content) appendSymbol("", os.path.basename(symbolPath)) return symbols
def processAlgorithm(self, progress): ''' Here is where the algorithm functionality takes places. This method is called when the algorithm is executed''' apik = pluginSetting("apiKey") if apik is None or apik == "":: ''' When there is a problem in a Processing algorithm, a GeoAlgorithmExecutionException should be raised. ''' raise GeoAlgorithmExecutionException("what3words API key is not defined")
def setTool(self): apikey = pluginSetting("apiKey") if apikey is None or apikey == "": self._showMessage( 'what3words API key is not set. Please set it and try again.', QgsMessageBar.WARNING) return #Create the map tool if needed if self.mapTool is None: self.mapTool = W3WMapTool(iface.mapCanvas()) #Change the menu item so it shows that the tool is active self.toolAction.setChecked(True) #Set the tool as the active map tool iface.mapCanvas().setMapTool(self.mapTool)
def endCreateAppListener(self, success, reason): self.onCreatingApp = False # reset button status and cursor self.buttonCreateOrStopApp.setText(self.createAppButtonText) QApplication.restoreOverrideCursor() from pubsub import pub pub.unsubscribe(self.endCreateAppListener, utils.topics.endFunction) if success: if pluginSetting("compileinserver"): QMessageBox.information( self, self.tr("Web app"), self.tr("Web app was correctly created and built.")) else: QMessageBox.information( self, self.tr("Web app"), self. tr("Web app file were correctly created.\n" "A web app can be built from them using Boundless WebSDK" )) elif reason: QgsMessageLog.logMessage("WebAppBuilder: {}".format(reason), level=QgsMessageLog.CRITICAL) if 'Request cancelled by user' in reason: # do nothing pass elif 'Cannot post preview webapp: Network error #5: Operation canceled' in reason: QMessageBox.critical( self, self.tr("Error creating web app"), self. tr("Network error due to a timeout.\n" "Please configure a longer timeout going to:\n" "Settings->Options->Network->Timeout for network requests (ms)." )) elif 'Permission denied' in reason: QMessageBox.critical( self, self.tr("Error creating web app"), self.tr( "Permission denied with current Connect credentials")) else: QMessageBox.critical( self, self.tr("Error creating web app"), self. tr("Could not create web app.\nCheck the QGIS log for more details." ))
def createOrStopApp(self): # check if app is compiling if self.onCreatingApp: stopAppCreation() return # start compilation try: appdef = self.createAppDefinition() problems = checkAppCanBeCreated(appdef) if pluginSetting("compileinserver"): QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) try: checkSDKServerVersion() except VersionMismatchError, e: problems.append(str(e)) except Exception, e: QApplication.restoreOverrideCursor() QMessageBox.warning(self, "Problem checking SDK version", str(e), QMessageBox.Close) return
def checkSDKServerVersion(): localVersion = utils.sdkVersion() token = utils.getToken() headers = {} headers["authorization"] = "Bearer {}".format(token) nam = NetworkAccessManager(debug=pluginSetting("logresponse")) try: resp, text = nam.request(wabVersionUrl(), headers=headers) except Exception as e: # check if 401/403 => probably token expired permissionDenied = utils.isPermissionDenied(str(e)) if not permissionDenied: raise e else: # renew token and try again utils.resetCachedToken() token = utils.getToken() # retry call headers["authorization"] = "Bearer {}".format(token) try: resp, text = nam.request(wabVersionUrl(), headers=headers) except Exception as e: # check if 401/403 => probably token expired permissionDenied = utils.isPermissionDenied(str(e)) if not permissionDenied: raise e else: raise Exception( "Permission denied with current Connect credentials") remoteVersion = json.loads(text)["boundless-sdk"] if localVersion != remoteVersion: raise VersionMismatchError( "The server SDK version (%s) is different from the expected version (%s)" % (remoteVersion, localVersion))
except Exception as ex: errMessage = str(ex) QMessageBox.warning(self, "Need Connect credentials", errMessage, QMessageBox.Close) return if problems: dlg = AppDefProblemsDialog(problems) dlg.exec_() if not dlg.ok: return # now ask where to store app folder = askForFolder(self, "Select folder to store app") if folder: if os.path.exists(os.path.join( folder, "webapp")) and pluginSetting("overwritewarning"): ret = QMessageBox.warning( self, "Output folder", " The selected folder already contains a 'webapp' subfolder.\n" "Do you confirm that you want to overwrite it?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if ret == QMessageBox.No: return # set buttons status self.setButtonsEnabled( status=False, excludeList=[self.buttonCreateOrStopApp]) self.createAppButtonText = self.buttonCreateOrStopApp.text() self.buttonCreateOrStopApp.setText(self.tr("Stop")) try:
def wabVersionUrl(): return urllib.parse.unquote( pluginSetting("sdkendpoint").rstrip("/") + "/version")
def wabCompilerUrl(): return urllib.parse.unquote( pluginSetting("sdkendpoint").rstrip("/") + "/package")
def authUrl(): return urllib.parse.unquote(pluginSetting("tokenendpoint"))
def writeWebApp(appdef, folder, forPreview, progress): """WriteApp end is notified using pub.sendMessage(utils.topics.endWriteWebApp, success=[True, False], reason=[str|None]) """ progress.setText("Copying resources files") dst = os.path.join(folder, "webapp") if os.path.exists(dst): shutil.rmtree(dst) QDir().mkpath(dst) sdkFolder = os.path.join(os.path.dirname(__file__), "websdk_full") if forPreview: shutil.copy(os.path.join(sdkFolder, "full-debug.js"), dst) QDir().mkpath(os.path.join(dst, "data")) jsFolder = os.path.join(os.path.dirname(__file__), "js") jsDstFolder = os.path.join(dst, "resources","js") shutil.copytree(jsFolder, jsDstFolder) cssFolder = os.path.join(os.path.dirname(__file__), "css") cssDstFolder = os.path.join(dst, "resources","css") shutil.copytree(cssFolder, cssDstFolder) shutil.copy(os.path.join(sdkFolder, "ol.css"), cssDstFolder) layers = appdef["Layers"] exportLayers(layers, dst, progress, appdef["Settings"]["Precision for GeoJSON export"], appdef["Settings"]["App view CRS"], forPreview) class App(): tabs = [] ol3controls = [] tools = [] panels = [] mappanels = [] variables = [] scripts = [] scriptsbody = [] posttarget = [] imports = [] aftermap = [] def newInstance(self): _app = App() _app.tabs = list(self.tabs) _app.ol3controls = list(self.ol3controls) _app.tools = list(self.tools) _app.panels = list(self.panels) _app.aftermap = list(self.aftermap) _app.mappanels = list(self.mappanels) _app.variables = list(self.variables) _app.scripts = list(self.scripts) _app.scriptsbody = list(self.scriptsbody) _app.posttarget = list(self.posttarget) _app.imports = list(self.imports) return _app _app = App() exportStyles(layers, dst, appdef["Settings"], "timeline" in appdef["Widgets"], _app, progress) writeLayersAndGroups(appdef, dst, _app, forPreview, progress) widgets = sorted(appdef["Widgets"].values(), key=attrgetter('order')) for w in widgets: w.write(appdef, dst, _app, progress) writeCss(appdef, dst, widgets) baseTarget = "_self" if appdef["Settings"]["Open hyperlinks in"] == 0 else "_blank" _app.scripts.append("<base target='%s'>" % baseTarget) if forPreview: app = _app.newInstance() writeJs(appdef, dst, app, progress) app.scriptsbody.extend(['<script src="full-debug.js"></script>', '<script src="app_prebuilt.js"></script>']) for layer in appdef["Layers"]: if layer.layer.type() == layer.layer.VectorLayer: app.scriptsbody.append('<script src="./data/lyr_%s.js"></script>' % safeName(layer.layer.name())) writeHtml(appdef, dst, app, progress, "index_debug.html") pub.sendMessage(utils.topics.endWriteWebApp, success=True, reason=None) else: app = _app.newInstance() writeJsx(appdef, dst, app, progress) app = _app.newInstance() app.scriptsbody.extend(['<script src="/loader.js"></script>', '<script src="/build/app-debug.js"></script>']) writeHtml(appdef, dst, app, progress, "index.html") if pluginSetting("compileinserver"): # apply SDK compilation to the saved webapp pub.subscribe(endAppSDKificationListener, utils.topics.endAppSDKification) try: global __appSDKification_doAgain __appSDKification_doAgain = False appSDKification(dst, progress) except Exception as e: pub.sendMessage(utils.topics.endAppSDKification, success=False, reason=str(e)) else: pub.sendMessage(utils.topics.endWriteWebApp, success=True, reason=None)
def _setWrongSdkEndpoint(): global _sdkEndpoint _sdkEndpoint = pluginSetting("sdkendpoint") setPluginSetting("sdkendpoint", "wrong")
def __init__(self, canvas): QgsMapTool.__init__(self, canvas) self.setCursor(Qt.CrossCursor) apiKey = pluginSetting("apiKey") self.w3w = what3words(apikey=apiKey)
def checkAppCanBeCreated(appdef, forPreview=False): ##viewCrs = appdef["Settings"]["App view CRS"] jsonp = appdef["Settings"]["Use JSONP for WFS connections"] problems = [] layers = appdef["Layers"] widgets = appdef["Widgets"].values() for w in widgets: w.checkProblems(appdef, problems, forPreview) themeModule = importlib.import_module("webappbuilder.themes." + appdef["Settings"]["Theme"]) themeModule.checkProblems(appdef, problems) def getSize(lyr): ptsInFeature = 1 if lyr.geometryType( ) == QGis.Point else 10 #quick estimate... return lyr.featureCount() * (ptsInFeature + lyr.pendingFields().size()) MAXSIZE = 30000 for applayer in layers: if applayer.layer.type( ) == applayer.layer.VectorLayer and applayer.layer.providerType( ).lower() != "wfs": if getSize(applayer.layer) > MAXSIZE: problems.append( "Layer %s might be too big for being loaded directly from a file." % applayer.layer.name()) nam = NetworkAccessManager(debug=pluginSetting("logresponse")) for applayer in layers: layer = applayer.layer if layer.providerType().lower() == "wms": try: source = layer.source() url = re.search( r"url=(.*?)(?:&|$)", source).groups(0)[0] + "?REQUEST=GetCapabilities" r, content = run( lambda: nam.request(url, headers={"origin": "null"})) cors = r.headers.get("Access-Control-Allow-Origin", "").lower() if cors not in ["null", "*"]: problems.append( "Server for layer %s is not allowed to accept cross-origin requests." " Popups and printing might not work correctly for that layer." % layer.name()) except: QgsMessageLog.logMessage( "Warning: cannot verify cross-origin configuration for layer '%s'." % layer.name(), level=QgsMessageLog.WARNING) for applayer in layers: layer = applayer.layer if layer.providerType().lower() == "wfs": datasourceUri = QgsDataSourceURI(layer.source()) url = datasourceUri.param("url") or layer.source().split("?")[0] url = url + "?service=WFS&version=1.1.0&REQUEST=GetCapabilities" try: if jsonp: r, content = run(lambda: nam.request(url)) if "text/javascript" not in r.headers.values(): problems.append( "Server for layer %s does not support JSONP. WFS layer won't be correctly loaded in Web App." % layer.name()) else: r, content = run( lambda: nam.request(url, headers={"origin": "null"})) cors = r.headers.get("Access-Control-Allow-Origin", "").lower() if cors not in ["null", "*"]: problems.append( "Server for layer %s is not allowed to accept cross-origin requests." % layer.name()) except: QgsMessageLog.logMessage( "Warning: cannot verify if WFS layer server has the required configuration. Layer: '%s'." % layer.name(), level=QgsMessageLog.WARNING) if layer.type() != layer.VectorLayer: continue renderer = applayer.layer.rendererV2() allowed = [ QgsSingleSymbolRendererV2, QgsCategorizedSymbolRendererV2, QgsGraduatedSymbolRendererV2, QgsHeatmapRenderer, QgsRuleBasedRendererV2 ] try: allowed.append(QgsNullSymbolRenderer) except: pass if not isinstance(renderer, tuple(allowed)): problems.append( "Symbology used by layer %s includes unsupported elements." "Only single symbol, categorized, graduated, heatmap and rule-based renderers are supported." "This layer will not be correctly styled in the web app." % layer.name()) if isinstance(renderer, QgsRuleBasedRendererV2): rules = renderer.rootRule().children() for rule in rules: expr = rule.filterExpression() unsupported = is_expression_supported(expr) if unsupported: problems.append( "The expression '%s' has unsupported functions: %s" % (expr, ", ".join(unsupported))) if layer.hasLabelsEnabled(): problems.append( "Layer %s uses old-style labeling. Labels might not be correctly rendered in the web app." % layer.name()) if str(layer.customProperty("labeling/enabled")).lower() == "true": if unicode(layer.customProperty( "labeling/isExpression")).lower() == "true": expr = layer.customProperty("labeling/fieldName") unsupported = is_expression_supported(expr) if unsupported: problems.append( "The expression '%s' has unsupported functions: %s" % (expr, ", ".join(unsupported))) #TODO: check that layers using time attributes are not published using WMS hasTimeInfo = False for applayer in layers: if applayer.timeInfo is not None: hasTimeInfo = True break if hasTimeInfo and "timeline" not in appdef["Widgets"]: problems.append( "There are layers with time information, but timeline widget is not used." ) if "timeline" in appdef["Widgets"]: for applayer in layers: layer = applayer.layer if layer.providerType().lower() == "wms": try: source = layer.source() url = re.search(r"url=(.*?)(?:&|$)", source).groups(0)[0] layernames = re.search(r"layers=(.*?)(?:&|$)", source).groups(0)[0] r, content = nam.request( url + "?service=WMS&request=GetCapabilities") root = ET.fromstring( re.sub('\\sxmlns="[^"]+"', '', r.content)) for layerElement in root.iter('Layer'): name = layerElement.find("Name").text if name == layernames: # look for discrete values time = layerElement.find('Extent') if time is not None: applayer.timeInfo = time hasTimeInfo = True # look for interval values time = layerElement.find('Dimension') if time is not None and time.attrib[ 'name'] == 'time': applayer.timeInfo = '"{}"'.format(time.text) hasTimeInfo = True except: #we swallow error, since this is not a vital info to add, so the app can still be created. pass if not hasTimeInfo: problems.append( "Timeline widget is used but there are no layers with time information" ) return problems