class Search(html5.Div): def __init__(self, *args, **kwargs): super(Search, self).__init__(*args, **kwargs) self.startSearchEvent = EventDispatcher("startSearch") self.addClass("flr-search") self.searchLbl = html5.H2() self.searchLbl.appendChild(html5.TextNode( translate("Fulltext search"))) self.searchLbl.addClass("flr-search-label") self.appendChild(self.searchLbl) self.searchInput = Input() self.searchInput["type"] = "text" self.appendChild(self.searchInput) self.btn = Button(translate("Search"), callback=self.doSearch) self.appendChild(self.btn) self.sinkEvent("onKeyDown") self.last_search = "" def doSearch(self, *args, **kwargs): if self.searchInput["value"] != self.last_search: if len(self.searchInput["value"]): self.startSearchEvent.fire(self.searchInput["value"]) else: self.resetSearch() self.last_search = self.searchInput["value"] def resetSearch(self): self.startSearchEvent.fire(None) def onKeyDown(self, event): if html5.isReturn(event): self.doSearch() event.preventDefault() event.stopPropagation() def resetLoadingState(self): if "is-loading" in self.btn["class"]: self.btn.removeClass("is-loading") def reevaluate(self): self.doSearch() def focus(self): self.searchInput.focus()
class Uploader(Progress): """ Uploads a file to the server while providing visual feedback of the progress. """ def __init__(self, file, node, context=None, showResultMessage=True, *args, **kwargs): """ :param file: The file to upload :type file: A javascript "File" Object :param node: Key of the desired node of our parents tree application or None for an anonymous upload. :type node: str or None :param showResultMessage: if True replaces progressbar with complete message on success :type showResultMessage: bool """ super(Uploader, self).__init__() self.uploadSuccess = EventDispatcher("uploadSuccess") self.uploadFailed = EventDispatcher("uploadFailed") self.responseValue = None self.targetKey = None self.showResultMessage = showResultMessage self.context = context r = NetworkService.request("file", "getUploadURL", params={"node": node} if node else {}, successHandler=self.onUploadUrlAvailable, failureHandler=self.onFailed, secure=True ) r.file = file r.node = node self.node = node # self.parent().addClass("is-uploading") def onUploadUrlAvailable(self, req): """ Internal callback - the actual upload url (retrieved by calling /file/getUploadURL) is known. """ params = NetworkService.decode(req)["values"] formData = html5.jseval("new FormData();") #if self.context: # for k, v in self.context.items(): # formData.append(k, v) #if req.node and str(req.node) != "null": # formData.append("node", req.node) for key, value in params["params"].items(): if key == "key": self.targetKey = value[:-16] # Truncate source/file.dat fileName = req.file.name value = value.replace("file.dat", fileName) formData.append(key, value) formData.append("file", req.file) self.xhr = html5.jseval("new XMLHttpRequest()") self.xhr.open("POST", params["url"]) self.xhr.onload = self.onLoad self.xhr.upload.onprogress = self.onProgress self.xhr.send(formData) def onSkeyAvailable(self, req): """ Internal callback - the Security-Key is known. # Only for core 2.x needed """ formData = html5.jseval("new FormData();") formData.append("file", req.file) if self.context: for k, v in self.context.items(): formData.append(k, v) if req.node and str(req.node) != "null": formData.append("node", req.node) formData.append("skey", NetworkService.decode(req)) self.xhr = html5.jseval("new XMLHttpRequest()") self.xhr.open("POST", req.destUrl) self.xhr.onload = self.onLoad self.xhr.upload.onprogress = self.onProgress self.xhr.send(formData) def onLoad(self, *args, **kwargs): """ Internal callback - The state of our upload changed. """ if self.xhr.status in [200, 204]: NetworkService.request( "file", "add", { "key": self.targetKey, "node": self.node, "skelType": "leaf" }, successHandler=self.onUploadAdded, secure=True ) else: DeferredCall(self.onFailed, self.xhr.status, _delay=1000) def onUploadAdded(self, req): self.responseValue = NetworkService.decode(req) DeferredCall(self.onSuccess, _delay=1000) def onProgress(self, event): """ Internal callback - further bytes have been transmitted """ if event.lengthComputable: complete = int(event.loaded / event.total * 100) self["value"] = complete self["max"] = 100 def onSuccess(self, *args, **kwargs): """ Internal callback - The upload succeeded. """ if isinstance(self.responseValue["values"], list): for v in self.responseValue["values"]: self.uploadSuccess.fire(self, v) else: self.uploadSuccess.fire(self, self.responseValue["values"]) NetworkService.notifyChange("file") if self.showResultMessage: self.replaceWithMessage("Upload complete", isSuccess=True) def onFailed(self, errorCode, *args, **kwargs): if self.showResultMessage: self.replaceWithMessage("Upload failed with status code %s" % errorCode, isSuccess=False) self.uploadFailed.fire(self, errorCode) def replaceWithMessage(self, message, isSuccess): self.parent().removeClass("is-uploading") self.parent().removeClass("log-progress") if isSuccess: self.parent().addClass("log-success") else: self.parent().addClass("log-failed") msg = html5.Span() msg.appendChild(html5.TextNode(message)) self.parent().appendChild(msg) self.parent().removeChild(self)
class viurForm(html5.Form): """ Handles an input form for a VIUR skeleton. """ def __init__(self, formName=None, moduleName=None, actionName="add", skel=None, structure=None, visible=(), ignore=(), hide=(), defaultValues=()): super().__init__() self.formName = formName self.moduleName = moduleName self.actionName = actionName self.bones = {} self.skel = skel self.errors = [] self.visible = visible self.ignore = ignore self.hide = hide self.defaultValues = defaultValues if structure: self.structure = {k: v for k, v in structure} self.formSuccessEvent = EventDispatcher("formSuccess") self.formSuccessEvent.register(self) self.addClass("form") self.sinkEvent("onChange") def onChange(self, event): self.applyVisiblity() def _setModulename(self, val): self.moduleName = val def _setActionname(self, val): self.actionName = val def _setFormname(self, val): self.formName = val def buildForm(self): for key, bone in self.structure.items(): if key in self.ignore: continue elif self.visible and key not in self.visible: continue boneValue = None if key in self.defaultValues: boneValue = self.defaultValues[key] bonefield = boneField(key, self, boneValue) self.appendChild(bonefield) submitbtn = sendForm(text="speichern", form=self) self.appendChild(submitbtn) def buildInternalForm(self): for key, bone in self.structure.items(): if key in self.ignore: continue elif self.visible and key not in self.visible: continue bonefield = boneField(key, self) bonefield.onAttach() #we need a better solution self.appendChild(bonefield) def registerField(self, key, widget): if key in self.ignore: return 0 elif self.visible and key not in self.visible: return 0 if key in self.bones: logging.debug( "Double field definition in {}!, only first field will be used", self) return 0 self.bones.update({key: widget}) def applyVisiblity(self): #only PoC! #WIP for key, boneField in self.bones.items(): codestr = getattr(boneField, "visibleif", None) if not codestr: continue from flare.config import conf seInst = conf["safeEvalInstance"] seResult = seInst.execute(seInst.compile(codestr), self.collectCurrentFormValues()) widget = boneField.bonewidget if not seResult: boneField.hide() else: boneField.show() def submitForm(self): res = self.collectCurrentFormValues() NetworkService.request( self.moduleName, self.actionName, res, secure=True, #always with fresh skey successHandler=self.actionSuccess, failureHandler=self.actionFailed) return res def collectCurrentFormValues(self): res = {} if "key" in self.skel and self.skel["key"]: res["key"] = self.skel["key"] for key, boneField in self.bones.items(): widget = boneField.bonewidget # ignore the key, it is stored in self.key, and read-only bones if key == "key" or widget.bone.readonly: continue try: res[key] = widget.serialize() if res[key] is None: res[key] = "" except InvalidBoneValueException: pass # if validityCheck: # return None return res def actionSuccess(self, req): resp = NetworkService.decode(req) logging.debug("actionSuccess: %r", resp) ''' severity cases: NotSet = 0 InvalidatesOther = 1 <-- relevant Empty = 2 Invalid = 3 <-- relevant ''' if "action" in resp and resp["action"].endswith("Success"): # form approved: Let's store the new skeleton values and fire the success event self.skel = resp["values"] self.formSuccessEvent.fire(self) else: #form rejected self.errors = resp["errors"] for error in self.errors: if error["fieldPath"] in self.bones: boneField = self.bones[ error["fieldPath"]] # todo dependency errors if (error["severity"]%2 == 0 and boneField["required"]) or\ (error["severity"]%2 == 1): #invalid boneField.setInvalid() else: boneField.setValid() self.createFormErrorMessage() def createFormSuccessMessage(self): try: self.removeChild(self.errorhint) except: pass if "successhint" not in dir(self): # language=HTML self.prependChild(''' <div [name]="successhint" class="msg is-active msg--success ">Erfolgreich gespeichert!</div> ''') def createFormErrorMessage(self): try: self.removeChild(self.successhint) except: pass if "errorhint" not in dir(self): #language=HTML self.prependChild(''' <div [name]="errorhint" class="msg is-active msg--error "></div> ''') self.errorhint.removeAllChildren() for error in self.errors: if error["fieldPath"] in self.bones: boneField = self.bones[ error["fieldPath"]] # todo dependency errors if error["severity"] == 1 or error["severity"] == 3: # invalid #language=HTML self.errorhint.appendChild( '''<span class="flr-bone--error">{{boneDescr}}: {{error}} </span>''', boneDescr=boneField.structure[boneField.boneName].get( "descr", boneField.boneName), error=error["errorMessage"]) def actionFailed(self, req, *args, **kwargs): logging.debug("FAILED: %r", req) def onFormSuccess(self, event): self.createFormSuccessMessage()
class NetworkService(object): """ Generic wrapper around ajax requests. Handles caching and multiplexing multiple concurrent requests to the same resource. It also acts as the central proxy to notify currently active widgets of changes made to data on the server. """ changeListeners = [ ] # All currently active widgets which will be informed of changes made host = "" prefix = "/json" defaultFailureHandler = NiceError retryCodes = [0, -1] retryMax = 3 retryDelay = 5000 @staticmethod def notifyChange(module, **kwargs): """ Broadcasts a change made to data of module 'module' to all currently registered changeListeners. :param module: Name of the module where the change occured :type module: str """ for c in NetworkService.changeListeners: c.onDataChanged(module, **kwargs) @staticmethod def registerChangeListener(listener): """ Registers object 'listener' for change notifications. 'listener' must provide an 'onDataChanged' function accepting one parameter: the name of the module. Does nothing if that object has already registered. :param listener: The object to register :type listener: object """ if listener in NetworkService.changeListeners: return NetworkService.changeListeners.append(listener) @staticmethod def removeChangeListener(listener): """ Unregisters the object 'listener' from change notifications. :param listener: The object to unregister. It must be currently registered. :type listener: object """ assert listener in NetworkService.changeListeners, "Attempt to remove unregistered listener %s" % str( listener) NetworkService.changeListeners.remove(listener) @staticmethod def genReqStr(params): boundary_str = "---" + ''.join([ random.choice(string.ascii_lowercase + string.ascii_uppercase + string.digits) for x in range(13) ]) boundary = boundary_str res = f"Content-Type: multipart/mixed; boundary=\"{boundary}\"\r\nMIME-Version: 1.0\r\n" res += "\r\n--" + boundary def expand(key, value): ret = "" if all([x in dir(value) for x in ["name", "read"]]): # File type = "application/octet-stream" filename = os.path.basename(value.name).decode( sys.getfilesystemencoding()) ret += \ f"\r\nContent-Type: {type}" \ f"\r\nMIME-Version: 1.0" \ f"\r\nContent-Disposition: form-data; name=\"{key}\"; filename=\"{filename}\"\r\n\r\n" ret += str(value.read()) ret += '\r\n--' + boundary elif isinstance(value, list): if any([isinstance(entry, dict) for entry in value]): for idx, entry in enumerate(value): ret += expand(key + "." + str(idx), entry) else: for entry in value: ret += expand(key, entry) elif isinstance(value, dict): for key_, entry in value.items(): ret += expand(((key + ".") if key else "") + key_, entry) else: ret += \ "\r\nContent-Type: application/octet-stream" \ "\r\nMIME-Version: 1.0" \ f"\r\nContent-Disposition: form-data; name=\"{key}\"\r\n\r\n" ret += str(value) if value is not None else "" ret += '\r\n--' + boundary return ret for key, value in params.items(): res += expand(key, value) res += "--\r\n" # fixme: DEBUG! #print(res) return res, boundary @staticmethod def decode(req): """ Decodes a response received from the server (ie parsing the json) :type req: Instance of NetworkService response :returns: object """ return json.loads(req.result) @staticmethod def isOkay(req): answ = NetworkService.decode(req) return isinstance(answ, str) and answ == "OKAY" @staticmethod def urlForArgs(module, path): """ Constructs the final url for that request. If module is given, it prepends "/prefix" If module is None, path is returned unchanged. :param module: Name of the target module or None :type module: str or None :param path: Path (either relative to 'module' or absolute if 'module' is None :type path: str :returns: str """ if module: href = "%s/%s/%s" % (NetworkService.prefix, module, path) else: href = path if not href.startswith("/"): href = "/" + href return NetworkService.host + href def __init__(self, module, url, params, successHandler, failureHandler, finishedHandler, modifies, secure, kickoff): """ Constructs a new NetworkService request. Should not be called directly (use NetworkService.request instead). """ super(NetworkService, self).__init__() self.result = None self.status = None self.waitingForSkey = False self.module = module self.url = url if params and not isinstance(params, dict): self.url += "/%s" % params params = {} self.params = params self.successHandler = [successHandler] if successHandler else [] self.failureHandler = [failureHandler] if failureHandler else [] self.finishedHandler = [finishedHandler] if finishedHandler else [] self.requestFinishedEvent = EventDispatcher('finished') self.requestFinishedEvent.register(self) self.modifies = modifies self.secure = secure self.kickoffs = 0 if kickoff: self.kickoff() def kickoff(self): self.status = "running" self.kickoffs += 1 if self.secure: self.waitingForSkey = True self.doFetch( "%s%s/skey" % (NetworkService.host, NetworkService.prefix), None, None) else: self.doFetch(NetworkService.urlForArgs(self.module, self.url), self.params, None) @staticmethod def request(module, url, params=None, successHandler=None, failureHandler=None, finishedHandler=None, modifies=False, secure=False, kickoff=True, group=None): """ Performs an AJAX request. Handles caching and security-keys. Calls made to this function are guaranteed to be async. :param module: Target module on the server. Set to None if you want to call anything else :type module: str or None :param url: The path (relative to module) or a full url if module is None :type url: None :param successHandler: function beeing called if the request succeeds. Must take one argument (the request). :type successHandler: callable :param failureHandler: function beeing called if the request failes. Must take two arguments (the request and an error-code). :type failureHandler: callable :param finishedHandler: function beeing called if the request finished (regardless wherever it succeeded or not). Must take one argument (the request). :type finishedHandler: callable :param modifies: If set to True, it will automatically broadcast an onDataChanged event for that module. :type modifies: bool :param secure: If true, include a fresh securitykey in this request. Defaults to False. :type secure: bool """ logging.debug("NetworkService.request module=%r, url=%r, params=%r", module, url, params) if group or secure: #secure and grouped requests will be handled later kickoff = False dataRequest = NetworkService(module, url, params, successHandler, failureHandler, finishedHandler, modifies, secure, kickoff) if group: group.addRequest(dataRequest) if not group and secure: # single secure requests will be queued to ensure a fresh skey #a group is triggered externally and processed sequentially skeyRequestQueue.append(dataRequest) return dataRequest def doFetch(self, url, params, skey): """ Internal function performing the actual AJAX request. """ if params: if skey: params["skey"] = skey contentType = None if isinstance(params, dict): multipart, boundary = NetworkService.genReqStr(params) contentType = "multipart/form-data; boundary=" + boundary + "; charset=utf-8" elif isinstance(params, bytes): contentType = "application/x-www-form-urlencoded" multipart = params else: multipart = params HTTPRequest("POST", url, self.onCompletion, self.onError, payload=multipart, content_type=contentType) else: if skey: if "?" in url: url += "&skey=%s" % skey else: url += "?skey=%s" % skey HTTPRequest("GET", url, self.onCompletion, self.onError) def onCompletion(self, text): """ Internal hook for the AJAX call. """ if self.waitingForSkey: self.waitingForSkey = False self.doFetch(NetworkService.urlForArgs(self.module, self.url), self.params, json.loads(text)) else: self.result = text self.status = "succeeded" try: for s in self.successHandler: s(self) for s in self.finishedHandler: s(self) self.requestFinishedEvent.fire(True) except: if self.modifies: DeferredCall( NetworkService.notifyChange, self.module, key=self.params.get("key") if self.params else None, action=self.url, _delay=2500) raise if self.modifies: DeferredCall( NetworkService.notifyChange, self.module, key=self.params.get("key") if self.params else None, action=self.url, _delay=2500) # Remove references to our handlers self.clear() def onError(self, text, code): """ Internal hook for the AJAX call. """ self.status = "failed" self.result = text logging.debug( "NetworkService.onError kickoffs=%r, retryMax=%r, code=%r, retryCodes=%r", self.kickoffs, self.retryMax, code, self.retryCodes) if self.kickoffs < self.retryMax and int(code) in self.retryCodes: # The following can be used to pass errors to a bugtracker service like Stackdriver or Bugsnag logError = None # html5.window.top.logError if logError and self.kickoffs == self.retryMax - 1: logError( "NetworkService.onError code:%s module:%s url:%s params:%s" % (code, self.module, self.url, self.params)) logging.error("error %d, kickoff %d, will retry now", code, self.kickoffs) DeferredCall(self.kickoff, _delay=self.retryDelay) return for s in self.failureHandler: s(self, code) if not self.failureHandler and self.defaultFailureHandler: self.defaultFailureHandler(code) if not self.defaultFailureHandler: self.clear() for s in self.finishedHandler: s(self) self.requestFinishedEvent.fire(False) def onTimeout(self, text): """ Internal hook for the AJAX call. """ self.onError(text, -1) def clear(self): self.successHandler = [] self.finishedHandler = [] self.failureHandler = [] self.params = None def onFinished(self, success): pass