From 763787ea02b43da4d6ef5bfe001fdc0a6ff70c56 Mon Sep 17 00:00:00 2001 From: Steve Nyemba Date: Tue, 1 Oct 2024 11:59:12 -0500 Subject: [PATCH 1/5] bug fix: plugin functions, streamline cli runner --- bin/qcms | 24 +++++-- cms/__init__.py | 110 +++++++++++++++++++++++++++++++++ cms/engine/plugins/__init__.py | 8 +++ cms/index.py | 81 +++++++----------------- meta/__init__.py | 2 +- 5 files changed, 161 insertions(+), 64 deletions(-) diff --git a/bin/qcms b/bin/qcms index 9d1da44..f274c5c 100755 --- a/bin/qcms +++ b/bin/qcms @@ -43,6 +43,13 @@ INVALID_FOLDER = """ # handling cli interface cli = typer.Typer() +def get_manifest (manifest): + if not manifest.endswith('json') and os.path.isdir(manifest): + manifest = manifest if not manifest.endswith(os.sep) else os.sep.join(manifest.split(os.sep)[:-1]) + return os.sep.join([manifest,'qcms-manifest.json']) + else: + return manifest + @cli.command(name="info") def _info(): @@ -174,6 +181,7 @@ def plug_info (manifest:Annotated[str,typer.Argument(help="path to manifest file """ Manage plugins list loaded plugins, """ + manifest = get_manifest(manifest) _config = config.get(manifest) _root = os.sep.join(manifest.split(os.sep)[:-1] + [_config['layout']['root'],'_plugins']) if os.path.exists(_root) : @@ -185,13 +193,15 @@ def plug_info (manifest:Annotated[str,typer.Argument(help="path to manifest file if not _plugins : _msg = f"""{FAILED} no plugins are loaded\n\t{manifest}""" else: - _data = plugins.stats(_plugins) + # _data = plugins.stats(_plugins) + _data = cms.Plugin.stats(_plugins) print (_data) _msg = f"""{PASSED} found a total of {_data.loaded.sum()} plugins loaded from {_data.shape[0]} file(s)\n\t{_root}""" if add in [True,False] and pointer : file,fnName = pointer.split('.') - _fnpointer = plugins.load(_root,file+'.py',fnName) + # _fnpointer = plugins.load(_root,file+'.py',fnName) + _fnpointer = cms.Plugin.load(_root,file+'.py',fnName) if _fnpointer and add: if file not in _plugins : _plugins[file] = [] @@ -269,7 +279,7 @@ def reload ( """ Reload a site/portal given the manifest ... """ - _config = config.get(path) + _config = config.get( get_manifest(path)) if 'key' in _config['system']['source'] : f = open(_config['system']['source']['key']) key = f.read() @@ -293,9 +303,10 @@ def bootup ( """ This function will launch a site/project given the location of the manifest file """ - if not manifest.endswith('json') and os.path.isdir(manifest): - manifest = manifest if not manifest.endswith(os.sep) else os.sep.join(manifest.split(os.sep)[:-1]) - manifest = os.sep.join([manifest,'qcms-manifest.json']) + # if not manifest.endswith('json') and os.path.isdir(manifest): + # manifest = manifest if not manifest.endswith(os.sep) else os.sep.join(manifest.split(os.sep)[:-1]) + # manifest = os.sep.join([manifest,'qcms-manifest.json']) + manifest = get_manifest(manifest) index.start(manifest,port) @cli.command(name='theme') def handle_theme ( @@ -307,6 +318,7 @@ def handle_theme ( This function will show the list available themes and can also set a theme in a manifest """ + manifest = get_manifest(manifest) _config = config.get(manifest) _root = os.sep.join( manifest.split(os.sep)[:-1]+[_config['layout']['root']]) diff --git a/cms/__init__.py b/cms/__init__.py index 5fe6631..1f00a43 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -6,8 +6,118 @@ import copy from jinja2 import Environment, BaseLoader, FileSystemLoader import importlib import importlib.util +import json +class Plugin : + # + # decorator for plugin functions, this is a preliminary to enable future developement + # + def __init__(self,**_args): + self._mimetype = _args['mimetype'] + if 'method' in _args : + _method = _args['method'] + if type(_method) != list : + _method = [_method] + else: + _method = ['POST','GET'] + self._method = _method + def __call__(self,_callback): + def wrapper(**_args): + return _callback(**_args) + setattr(wrapper,'method',self._method) + setattr(wrapper,'mimetype',self._mimetype) + return wrapper + @staticmethod + def load(_path,_filename,_name) : + """ + This function will load external module form a given location and return a pointer to a function in a given module + :path absolute path of the file (considered plugin) to be loaded + :name name of the function to be applied + """ + # _path = _args['path'] #os.sep.join([_args['root'],'plugin']) + + if os.path.isdir(_path): + files = os.listdir(_path) + if files : + files = [name for name in files if name.endswith('.py') and name == _filename] + if files: + _path = os.sep.join([_path,files[0]]) + else: + return None + else: + return None + #-- We have a file ... + # _name = _args['name'] + spec = importlib.util.spec_from_file_location(_filename, _path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + # + # we need to make sure we have the plugin decorator here to make sure + + return getattr(module,_name) if hasattr(module,_name) else None + + @staticmethod + def stats (_config) : + """ + Returns the statistics of the plugins + """ + _data = [] + for _name in _config : + _log = {"files":_name,"loaded":len(_config[_name]),"logs":json.dumps(_config[_name])} + _data.append(_log) + return pd.DataFrame(_data) + @staticmethod + def call(**_args): + """ + This function will execute a plugged-in function given + """ + # _uri = _args['uri'] if 'uri' in _args else '/'.join([_args['module'],_args['name']]) + _handler= _args['handler'] #-- handler + _request= _args['request'] + _uri = _args['uri'] #_request.path[1:] + # + # we need to update the _uri (if context/embeded apps) + # + _context = _handler.system()['context'] + if _context : + _uri = f'{_context}/{_uri}' + _plugins = _handler.plugins() + _code = 200 + if _uri in _plugins : + _config = _handler.config() #_args['config'] + + + _pointer = _plugins[_uri] + if hasattr(_pointer,'mimetype') and _request.method in _pointer.method: + # + # we constraint the methods given their execution ... + _mimeType = _pointer.mimetype + _data = _pointer(request=_request,config=_config) + else: + _mimeType = 'application/octet-stream' + try: + _data,_mimeType = _pointer(request=_request,config=_config) + + except Exception as e: + + _data = _pointer(request=_request,config=_config) + + pass + else: + _code = 404 + # + # We should generate a 500 error in this case with a message ... + # + _mimeType = 'plain/html' + _data = f""" + + """ + + return _data,_code,{'Content-Type':_mimeType} + pass # def _get_config (path) : # if os.path.exists(path) : diff --git a/cms/engine/plugins/__init__.py b/cms/engine/plugins/__init__.py index 6ada10b..86858f0 100644 --- a/cms/engine/plugins/__init__.py +++ b/cms/engine/plugins/__init__.py @@ -4,6 +4,11 @@ import importlib import importlib.util import os +# +# Defining the decorator to be used in plugins, this will enable simple loading and assigning mimetype to the output (if any) +# + + def stats (_config) : """ Returns the statistics of the plugins @@ -37,6 +42,9 @@ def load(_path,_filename,_name) : spec = importlib.util.spec_from_file_location(_filename, _path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) + # + # we need to make sure we have the plugin decorator here to make sure + return getattr(module,_name) if hasattr(module,_name) else None diff --git a/cms/index.py b/cms/index.py index dfc1abd..7d4f4cd 100644 --- a/cms/index.py +++ b/cms/index.py @@ -7,6 +7,7 @@ import flask import transport from transport import providers import cms +from cms import Plugin import sys import os import json @@ -144,28 +145,16 @@ def _dialog (app): _args['title'] = _id return render_template('dialog.html',**_args) #title=_id,html=_html) -@_app.route("/api//",defaults={'app':'main','key':None}) -@_app.route("//api//",defaults={'key':None}) -@_app.route("////",defaults={'key':None}) +@_app.route("/api//",defaults={'app':'main','key':None},methods=['GET','POST','DELETE','PUT']) +@_app.route("//api//",defaults={'key':None},methods=['GET','POST','DELETE','PUT']) +@_app.route("////",defaults={'key':None},methods=['GET','POST','DELETE','PUT']) def _delegate_call(app,key,module,name): _handler = _getHandler(app,key) - return _delegate(_handler,module,name) - -# @_app.route('/api//') -@_app.route("///api//", methods=['GET']) -@_app.route("/api//",defaults={'app_id':'main','key':None}) -@_app.route("//api//",defaults={'key':None}) - -def _api(app_id,key,module,name) : - """ - This endpoint will load a module and make a function call - :_module entry specified in plugins of the configuration - :_name name of the function to execute - """ - - _handler = _getHandler( app_id,key) - return _delegate(_handler,module,name) + # print (_handler.config()/) + uri = f'api/{module}/{name}' + return Plugin.call(uri=uri,handler=_handler,request=request) + # return _delegate(_handler,module,name) def _delegate(_handler,module,name): global _route @@ -186,10 +175,13 @@ def _delegate(_handler,module,name): # _data = pointer (**_args) # else: # _data = pointer() + if hasattr(pointer,'mimetype') : + _data = pointer(request=request,config=_handler.config()) + _mimeType = pointer.mimetype + else: + _data,_mimeType = pointer(request=request,config=_handler.config()) - _data,_mimeType = pointer(request=request,config=_handler.config()) - - _mimeType = 'application/octet-stream' if not _mimeType else _mimeType + _mimeType = 'application/octet-stream' if not _mimeType else _mimeType if type(_data) == pd.DataFrame : _data = _data.to_dict(orient='records') if type(_data) == list: @@ -201,18 +193,18 @@ def _delegate(_handler,module,name): # @_app.route('//api//',methods=['POST'],defaults={'key':None}) # @_app.route('///api//',methods=['POST'],defaults={'app_id':'main','key':None}) -@_app.route("///api//", methods=['POST']) -@_app.route("/api//",defaults={'app_id':'main','key':None},methods=['POST']) -@_app.route("//api//",defaults={'key':None},methods=['POST']) +# @_app.route("///api//", methods=['POST']) +# @_app.route("/api//",defaults={'app_id':'main','key':None},methods=['POST']) +# @_app.route("//api//",defaults={'key':None},methods=['POST']) -def _post (app_id,key,module,name): - # global _config - # global _route - # _handler = _route.get() - # app_id = '/'.join([app_id,key]) if key else app_id +# def _post (app_id,key,module,name): +# # global _config +# # global _route +# # _handler = _route.get() +# # app_id = '/'.join([app_id,key]) if key else app_id - _handler = _getHandler(app_id,key) - return _delegate(_handler,module,name) +# _handler = _getHandler(app_id,key) +# return _delegate(_handler,module,name) @_app.route('/version') def _version (): @@ -310,31 +302,6 @@ def _cms_page (app_id,resource): _args = _route.render(_uri,_title,session.get(app_id,'main')) return _args[_title],200 -# @_app.route('/set/') -# def set(id): -# global _route -# _setHandler(id) -# # _route.set(id) -# # _handler = _route.get() -# _handler = _getHandler() -# _context = _handler.system()['context'] -# _uri = f'/{_context}'.replace('//','/') -# return redirect(_uri) -# @_app.route('/') -# def _open(id): -# global _route -# # _handler = _route.get() - -# _handler = _getHandler() -# if id not in _route._apps : - -# _args = {'config':_handler.config(), 'layout':_handler.layout(),'system':_handler.system(skip=['source','app'])} -# return render_template("404.html",**_args) -# else: -# _setHandler(id) -# # _route.set(id) -# return _index() - @cli.command() def start ( diff --git a/meta/__init__.py b/meta/__init__.py index 5a70ba9..c3e7398 100644 --- a/meta/__init__.py +++ b/meta/__init__.py @@ -1,5 +1,5 @@ __author__ = "Steve L. Nyemba" -__version__= "2.1.6" +__version__= "2.2.0" __email__ = "steve@the-phi.com" __license__=""" Copyright 2010 - 2024, Steve L. Nyemba, Vanderbilt University Medical Center From 8884b8e02d393e2ce05da102aed03eb5bda75f35 Mon Sep 17 00:00:00 2001 From: Steve Nyemba Date: Tue, 22 Oct 2024 12:40:13 -0500 Subject: [PATCH 2/5] bug fixes & optimizations --- bin/qcms | 4 ++-- cms/__init__.py | 5 ++++- cms/index.py | 29 ++++++----------------------- cms/static/js/menu.js | 10 +++++++--- cms/templates/header.html | 2 +- cms/templates/index.html | 2 +- 6 files changed, 21 insertions(+), 31 deletions(-) diff --git a/bin/qcms b/bin/qcms index f274c5c..333ae4b 100755 --- a/bin/qcms +++ b/bin/qcms @@ -280,7 +280,7 @@ def reload ( Reload a site/portal given the manifest ... """ _config = config.get( get_manifest(path)) - if 'key' in _config['system']['source'] : + if 'source' in _config['system'] and 'key' in _config['system']['source'] : f = open(_config['system']['source']['key']) key = f.read() f.close() @@ -293,7 +293,7 @@ def reload ( _msg = f"""{FAILED} failed to reload, status code {resp.status_code}\n{url} """ else: - _msg = f"""{FAILED} no secure key found in manifest""" + _msg = f"""{FAILED} no secure key found in manifest to request reload""" print (_msg) @cli.command(name="bootup") def bootup ( diff --git a/cms/__init__.py b/cms/__init__.py index 1f00a43..42ce092 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -102,7 +102,10 @@ class Plugin : except Exception as e: _data = _pointer(request=_request,config=_config) - + if type(_data) == pd.DataFrame : + _data = _data.to_json(orient='records') + elif type(_data) in [dict,list] : + _data = json.dumps(_data) pass else: _code = 404 diff --git a/cms/index.py b/cms/index.py index 7d4f4cd..bdecc31 100644 --- a/cms/index.py +++ b/cms/index.py @@ -46,8 +46,10 @@ def _getHandler (app_id,resource=None) : _id = _getId(app_id,resource) return _route._apps[_id] -def _getId(app_id,resource): - return '/'.join([app_id,resource]) if resource else app_id +def _getId(app_id,app_x): + if app_x not in [None,''] : + return '/'.join([app_id,app_x]) + return app_id def _setHandler (app_id,resource) : session['app_id'] = _getId(app_id,resource) @@ -96,33 +98,14 @@ def _getIndex (app_id ,resource=None): @_app.route("/") def _index (): return _getIndex('main') -# def _xindex (): -# _handler = _getHandler() -# _config = _handler.config() -# global _route -# # print ([' serving ',session.get('app_id','NA'),_handler.layout()['root']]) -# _args={'system':_handler.system(skip=['source','app','data']),'layout':_handler.layout()} - -# try: -# uri = os.sep.join([_config['layout']['root'], _config['layout']['index']]) -# _index_page = "index.html" - -# _args = _route.render(uri,'index',session.get('app_id','main')) -# # _setHandler('main') -# except Exception as e: -# # print () -# print (e) -# _index_page = "404.html" - -# return render_template(_index_page,**_args),200 if _index_page != "404.html" else 200 @_app.route("//") @_app.route("/",defaults={'resource':None}) def _aindex (app,resource=None): _handler = _getHandler(app,resource) + _setHandler(app,resource) _html,_code = _getIndex(app,resource) - return _html,_code # @_app.route('/id/') # def people(uid): @@ -147,7 +130,7 @@ def _dialog (app): @_app.route("/api//",defaults={'app':'main','key':None},methods=['GET','POST','DELETE','PUT']) @_app.route("//api//",defaults={'key':None},methods=['GET','POST','DELETE','PUT']) -@_app.route("////",defaults={'key':None},methods=['GET','POST','DELETE','PUT']) +@_app.route("///api//",methods=['GET','POST','DELETE','PUT']) def _delegate_call(app,key,module,name): _handler = _getHandler(app,key) diff --git a/cms/static/js/menu.js b/cms/static/js/menu.js index 1e7517d..ddeb441 100644 --- a/cms/static/js/menu.js +++ b/cms/static/js/menu.js @@ -313,9 +313,13 @@ var QCMSTabs = function(_layout,_context,_clickEvent){ // _button._uri = _label._uri // if(this._layout.icons[text] != null) { - var _icon = jx.dom.get.instance('I') - _icon.className = this._layout.icons[text] - $(_label).append(_icon) + + if (this._layout.icon){ + var _icon = jx.dom.get.instance('I') + _icon.className = this._layout.icons[text] + $(_label).append(_icon) + } + text = ' ' + text // } diff --git a/cms/templates/header.html b/cms/templates/header.html index ee0bde5..4b86142 100644 --- a/cms/templates/header.html +++ b/cms/templates/header.html @@ -6,4 +6,4 @@
{{layout.header.title}}
{{layout.header.subtitle}}
-
\ No newline at end of file + diff --git a/cms/templates/index.html b/cms/templates/index.html index 9503e7f..6888011 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -62,7 +62,7 @@ Vanderbilt University Medical Center var _layout = {{layout|tojson}} - sessionStorage.setItem('{{system.id}}','{{system.context|safe}}') + //sessionStorage.setItem('{{system.id}}','{{system.context|safe}}') $(document).ready( function(){ bootup.init('{{system.id}}',_layout) From db9620b04dafe100c4031950362f8c5c3d1c5bc9 Mon Sep 17 00:00:00 2001 From: Steve Nyemba Date: Tue, 22 Oct 2024 12:40:55 -0500 Subject: [PATCH 3/5] version update .. --- meta/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meta/__init__.py b/meta/__init__.py index c3e7398..7c8e49e 100644 --- a/meta/__init__.py +++ b/meta/__init__.py @@ -1,5 +1,5 @@ __author__ = "Steve L. Nyemba" -__version__= "2.2.0" +__version__= "2.2.2" __email__ = "steve@the-phi.com" __license__=""" Copyright 2010 - 2024, Steve L. Nyemba, Vanderbilt University Medical Center From 3540f73a84ae1003530b5094373bd50169ebbf28 Mon Sep 17 00:00:00 2001 From: Steve Nyemba Date: Tue, 29 Oct 2024 19:27:23 -0500 Subject: [PATCH 4/5] bug fixes --- cms/engine/basic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/engine/basic.py b/cms/engine/basic.py index 7a1da60..ff09dcd 100644 --- a/cms/engine/basic.py +++ b/cms/engine/basic.py @@ -4,8 +4,8 @@ import io import copy from cms import disk, cloud from jinja2 import Environment, BaseLoader, FileSystemLoader -import importlib -import importlib.util +# import importlib +# import importlib.util """ There are four classes at play here: [ Initializer ] <|-- [ Module ] <|-- [ MicroService ] <--<>[ CMS ] From ac40d95aca496f17c2d1ef5ed22dd4ccb8fa693d Mon Sep 17 00:00:00 2001 From: Steve Nyemba Date: Mon, 4 Nov 2024 10:13:00 -0600 Subject: [PATCH 5/5] new: source code copy to clipboard, and css with font-awesome --- cms/static/css/source-code.css | 2 ++ cms/static/js/menu.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/cms/static/css/source-code.css b/cms/static/css/source-code.css index e46f72c..a78ff23 100644 --- a/cms/static/css/source-code.css +++ b/cms/static/css/source-code.css @@ -8,6 +8,8 @@ border-left:8px solid #CAD5E0; margin-left:10px; font-weight: bold; font-size:14px; } +.source-code .fa-copy {float:right; margin:4px; cursor:pointer} +/* .source-code .fa-copy:hover */ .editor { background-color:#f3f3f3; diff --git a/cms/static/js/menu.js b/cms/static/js/menu.js index ddeb441..04a2f1b 100644 --- a/cms/static/js/menu.js +++ b/cms/static/js/menu.js @@ -111,6 +111,11 @@ menu.events._dialog = function (_item,_context){ // var url = _args['url'] _item.type = (_item.type == null)? 'redirect' :_item.type var http = HttpClient.instance() + _regex = /uri=(.+)/; + if (_item.uri.match(_regex)) { + _seg = _item.uri.match(_regex) + _item.uri = _seg[_seg.length - 1] + } http.setHeader('uri',_item.uri) http.setHeader('dom',(_item.title)?_item.title:'dialog') // http.setHeader('dom',_args.text) @@ -121,6 +126,11 @@ menu.events._dialog = function (_item,_context){ }) } +menu.events._openTabs = function (_TabContentPane, _id) { + _id = _id[0] != '.' ? ('.'+_id) : _id + $(_TabContentPane).children().slideUp('fast') + $(_id).slideDown() +} menu.events._open = function (id,uri,_context){ id = id.replace(/ /g,'-') @@ -243,6 +253,7 @@ var QCMSBasic= function(_layout,_context,_clickEvent) { if(this.data.uri && this.data.type != 'open') { if (this.data.type == 'dialog') { + // console.log(this.data) menu.events._dialog(this.data,_context) }else{ menu.events._open(menu.utils.format(this.data.text),this.data.uri,_context) @@ -417,3 +428,22 @@ menu.init =function (_layout,_context){ } + + +/*** + * + * Source Code + */ +if (! code){ + var code = {} +} +code.copy = function(_node) { + var _code = $(_node.parentNode).text().trim().replace(/ {8}/g,'').replace(/ {4}/g,'\t').replace(/\r/g,'\n') + navigator.clipboard.writeText(_code); + $(_node).empty() + $(_node).html('') + setTimeout(function(){ + $(_node).empty() + $(_node).html('') + },750) +} \ No newline at end of file