diff --git a/bin/qcms b/bin/qcms index 3ac518e..b7e06f0 100755 --- a/bin/qcms +++ b/bin/qcms @@ -21,11 +21,14 @@ import cms from cms import index from cms.engine.config.structure import Layout, System -from cms.engine import project, config, themes, plugins +from cms.engine import project, themes +import cms.engine.config +import cms.engine.project import pandas as pd import requests - - +import plugin_ix +from rich.table import Table +from rich import print start = index.start __doc__ = f""" Built and designed by Steve L. Nyemba, steve@the-phi.com @@ -33,8 +36,10 @@ version {meta.__version__} {meta.__license__}""" -PASSED = ' '.join(['[',colored('\u2713', 'green'),']']) -FAILED= ' '.join(['[',colored('\u2717','red'),']']) +# PASSED = ' '.join(['[',colored('\u2713', 'green'),']']) +# FAILED= ' '.join(['[',colored('\u2717','red'),']']) +FAILED = '[ [red] \u2717 [/red] ]' +PASSED = '[ [green] \u2713 [/green] ]' INVALID_FOLDER = """ {FAILED} Unable to proceed, could not find project manifest. It should be qcms-manifest.json @@ -43,6 +48,20 @@ INVALID_FOLDER = """ # handling cli interface cli = typer.Typer() +def to_Table(df: pd.DataFrame): + """Displays a Pandas DataFrame as a rich table.""" + table = Table(show_header=True, header_style="bold magenta") + + for col in df.columns: + table.add_column(col) + + for _, row in df.iterrows(): + table.add_row(*row.astype(str).tolist()) + + # console.print(table) + return table + + 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]) @@ -63,9 +82,12 @@ def _info(): @cli.command(name='setup') # def set_app (host:str="0.0.0.0",context:str="",port:int=8084,debug=True): -def set_app (host:Annotated[str,typer.Argument(help="bind host IP address")]="0.0.0.0", - context:Annotated[str,typer.Argument(help="if behind a proxy server (no forward slash needed)")]="", - port:Annotated[int,typer.Argument(help="port on which to run the application")]=8084, +def set_app ( + manifest:Annotated[str,typer.Argument(help="path to manifest or manifest folder")], + port:int=typer.Option(default=None,help="port on which to run the application"), + host:str=typer.Option(default='0.0.0.0',help="bind host IP address"), + context:str=typer.Option(default='',help="if behind a proxy server (no forward slash needed)"), + debug:Annotated[bool,typer.Argument(help="set debug mode on|off")]=True): """ Setup application access i.e port, debug, and/or context @@ -73,27 +95,30 @@ def set_app (host:Annotated[str,typer.Argument(help="bind host IP address")]="0. """ global INVALID_FOLDER - _config = config.get() + path = get_manifest(manifest) + _config = cms.engine.config.get(path) if _config : + # _system = _config['system']['app'] _app = _config['system']['app'] - _app['host'] = host - _app['port'] = port - _app['debug'] = debug + _app['host'] = host if host != _app['host'] else _app['host'] + _app['port'] = port if port else _app['port'] + _app['debug'] = debug if _app['debug'] != debug else _app['debug'] _config['system']['context'] = context _config['app'] = _app - config.write(_config) - _msg = f"""{PASSED} Successful update, good job ! + cms.engine.config.write(_config,path) + _msg = f"""{PASSED} [bold] {_config['layout']['header']['title']}[/bold]: Successful update, good job ! """ else: _msg = INVALID_FOLDER print (_msg) @cli.command(name='cloud') # def set_cloud(path:str): #url:str,uid:str,token:str): -def set_cloud(path:Annotated[str,typer.Argument(help="path of the auth-file for the cloud")]): #url:str,uid:str,token:str): +def set_cloud(manifest:Annotated[str,typer.Argument(help="path of the auth-file for the cloud")]): #url:str,uid:str,token:str): """ Setup qcms to generate a site from files on nextcloud The path must refrence auth-file (data-transport) """ + path = get_manifest(manifest) if os.path.exists(path): f = open (path) _auth = json.loads(f.read()) @@ -101,11 +126,11 @@ def set_cloud(path:Annotated[str,typer.Argument(help="path of the auth-file for url = _auth['url'] if os.path.exists('qcms-manifest.json') : - _config = config.get() + _config = cms.engine.config.get() _config['system']['source'] = path #{'id':'cloud','auth':{'url':url,'uid':uid,'token':token}} - config.write(_config) + cms.engine.config.write(_config) title = _config['layout']['header']['title'] - _msg = f"""{PASSED} Successfully update, good job! + _msg = f"""{PASSED} [bold]{_config['system']['layout']['header']['title']}[/bold] : successfully update, good job! {url} """ else: @@ -132,16 +157,17 @@ def secure( f.write(str(uuid.uuid4())) f.close() # - _config = config.get(manifest) + manifest = get_manifest(manifest) + _config = cms.engine.config.get(manifest) if 'source' not in _config['system']: _config['system']['source'] = {'id':'disk'} _config['system']['source']['key'] = keyfile - config.write(_config,manifest) - _msg = f"""{PASSED} A key was generated and written to {keyfile} + cms.engine.config.write(_config,manifest) + _msg = f"""{PASSED} [bold]{_config['layout']['header']['title']}[/bold] : A key was generated and written to {keyfile} use this key in header to enable reload of the site ...` """ else: - _msg = f"""{FAILED} Could NOT generate a key, because it would seem you already have one + _msg = f"""{FAILED} [bold]{_config['system']['layout']['header']['title']}[/bold] : could [bold]NOT[/bold] generate a key, because it would seem you already have one Please manually delete {keyfile} @@ -173,63 +199,78 @@ def load(**_args): return getattr(module,_name) if hasattr(module,_name) else None + @cli.command(name='plugins') -def plug_info (manifest:Annotated[str,typer.Argument(help="path to manifest file")], +def plugin_manager (manifest:Annotated[str,typer.Argument(help="path to manifest file")], show:bool=typer.Option(default=False,help="list plugins loaded"), - add: Annotated[Optional[bool],typer.Option("--register/--unregister",help="add/remove a plugin to manifest use with --pointer option")] = None, + add: Annotated[Optional[bool],typer.Option("--register/--unregister",help="add/remove a plugin to manifest use with --pointer option")] = None, pointer:str=typer.Option(default=None,help="pointer is structured as 'filename.function'") ) : """ 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) : + _config = cms.engine.config.get(manifest) + if _config : + _root = os.sep.join(manifest.split(os.sep)[:-1] + [_config['layout']['root'],'_plugins']) + else : + _root = None + if _root and os.path.exists(_root) : # files = os.listdir(_root) _msg = f"""{FAILED} no operation was specified, please use --help option""" # if 'plugins' in _config : _plugins = _config['plugins'] if 'plugins' in _config else {} + if show : if not _plugins : _msg = f"""{FAILED} no plugins are loaded\n\t{manifest}""" else: - # _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}""" - + _data = [] + _plugConf = _config['plugins'] + + for _name in _plugConf : + _log = {"files":_name,"loaded":len(_plugConf[_name]),"functions":json.dumps(_plugConf[_name])} + _data.append(_log) + _data= pd.DataFrame(_data) + + # # # _data = plugins.stats(_plugins) + # # _data = cms.Plugin.stats(_plugins) + print (to_Table(_data)) + _msg = f"""{PASSED} [bold]{_config['layout']['header']['title']}[/bold]: 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('.') + + file,fnName = pointer.split('.') # _fnpointer = plugins.load(_root,file+'.py',fnName) - _fnpointer = cms.Plugin.load(_root,file+'.py',fnName) - if _fnpointer and add: + # _fnpointer = cms.Plugin.load(_root,file+'.py',fnName) + _ploader = plugin_ix.Loader(file = os.sep.join([_root,file+'.py'])) + + if add and _ploader.has(fnName): if file not in _plugins : _plugins[file] = [] if fnName not in _plugins[file] : _plugins[file].append(fnName) - _msg = f"""{PASSED} registered {pointer}, use the --show option to list loaded plugins\n{manifest} """ + _msg = f"""{PASSED} [bold]{_config['layout']['header']['title']}[/bold]: registered {pointer}, use the --show option to list loaded plugins""" else: - _msg = f"""{FAILED} could not register {pointer}, it already exists\n\t{manifest} """ + _msg = f"""{FAILED} [bold]{_config['layout']['header']['title']}[/bold]: could not register {pointer}, it already exists""" elif add is False and file in _plugins: - _plugins[file] = [_name.strip() for _name in _plugins[file] if _name.strip() != fnName.strip() ] - - _msg = f"""{PASSED} unregistered {pointer}, use the --show option to list loaded plugins\n{manifest} """ + _plugins[file] = [_name.strip() for _name in _plugins[file] if _name.strip() != fnName.strip() ] + _msg = f"""{PASSED} [bold]{_config['layout']['header']['title']}[/bold]: unregistered {pointer}, use the --show option to list loaded plugins """ - # - # We need to write this down !! + # # + # # We need to write this down !! if add in [True,False] : _config['plugins'] = _plugins - config.write(_config,manifest) + cms.engine.config.write(_config,manifest) # else: - # _msg = f"""{FAILED} no plugins are loaded\n\t{manifest}""" + # _msg = f"""{FAILED} [bold]{_config['layout']['header']['title']}[/bold]: no plugins are loaded """ print() print(_msg) else: - _msg = f"""{FAILED} no plugin folder found """ - pass + _msg = f"""{FAILED} No plugin folder could be found in {manifest}""" + print (_msg) @cli.command (name='create') @@ -263,7 +304,7 @@ def create(folder:Annotated[str,typer.Argument(help="path of the project folder" # # Setup Project on disk # - project.make(folder=folder,config=_config) + cms.engine.project.make(folder=folder,config=_config) print (f"""{PASSED} created project at {folder} """) else: print () @@ -281,7 +322,7 @@ def reload ( Reload a site/portal given the manifest ... """ path = get_manifest(path) - _config = config.get( path) + _config = cms.engine.config.get( path) if 'source' in _config['system'] and 'key' in _config['system']['source'] : _spath = _config['system']['source']['key'] # f = open(_config['system']['source']['key']) @@ -298,7 +339,7 @@ def reload ( url = f"http://localhost:{_port}/reload" resp = requests.post(url, headers={"key":key}) if resp.status_code == 200 : - _msg = f"""{PASSED} successfully reloaded {url}""" + _msg = f"""{PASSED} [bold]{_config['layout']['header']['title']}[/bold] : successfully reloaded {url}""" else: _msg = f"""{FAILED} failed to reload, status code {resp.status_code}\n{url} """ @@ -329,7 +370,7 @@ def handle_theme ( """ manifest = get_manifest(manifest) - _config = config.get(manifest) + _config = cms.engine.config.get(manifest) _root = os.sep.join( manifest.split(os.sep)[:-1]+[_config['layout']['root']]) if show : @@ -353,8 +394,8 @@ def handle_theme ( # values.sort() # _df['installed'] = values else: - _df = f"""{FAILED} No themes were found in registry,\ncurl {themes.URL}/api/themes/List (QCMS_HOST_URL should be set)""" - print (_df) + _df = f"""{FAILED} [bold]{_config['system']['layout']['header']['title']}[/bold] : No themes were found in registry,\ncurl {themes.URL}/api/themes/List (QCMS_HOST_URL should be set)""" + print (to_Table(_df)) if name and not show: # we need to install the theme, i.e download it and update the configuration # @@ -374,11 +415,11 @@ def handle_theme ( # Let us update the configuration file ... # _config['system']['theme'] = name - config.write(_config,manifest) - _msg = f"""{PASSED} successfully downloaded {name} \n{PASSED} updated manifest {manifest} + cms.engine.config.write(_config,manifest) + _msg = f"""{PASSED} [bold]{_config['system']['layout']['header']['title']}[/bold] : successfully downloaded {name} \n{PASSED} updated manifest {manifest} """ except Exception as e: - _msg = f"""{FAILED} operation failed "{str(e)}" + _msg = f"""{FAILED} [[bold]{_config['system']['layout']['header']['title']}[/bold] : operation failed "{str(e)}" """ pass diff --git a/cms/__init__.py b/cms/__init__.py index 42ce092..d621116 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -8,6 +8,8 @@ import importlib import importlib.util import json +from . import apexchart + class Plugin : # # decorator for plugin functions, this is a preliminary to enable future developement @@ -27,45 +29,45 @@ class Plugin : 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']) + # @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 + # 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 + # 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 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): @@ -122,59 +124,56 @@ class Plugin : return _data,_code,{'Content-Type':_mimeType} pass -# def _get_config (path) : -# if os.path.exists(path) : -# f = open(path) -# _conf = json.loads(f.read()) -# f.close() -# else: -# _conf = {} -# return _conf -# def _isvalid(_allowed,**_args): - -# if not list(set(_allowed) - set(_args.keys())) : -# _pargs = {} -# for key in _allowed : -# _pargs [key] = _args[key] -# return _pargs -# return False - -# def write_config(_config, path): -# f = open(path,'w') -# f.write( json.dumps(_config)) ; -# f.close() - -# def _system(**_args): -# """ -# Both version and context must be provided otherwise they are ignored -# :version -# :context context -# """ -# _info = _isvalid(['version','context'],**_args) -# _info['theme'] = 'default.css' -# _info = _info if _info else {'version':'0.0.0','context':'','theme':'default.css'} -# if _info : -# _info['logo'] = None if 'logo' not in _args else _args['logo'] -# # -# # There is an aggregation entry here in app -# _appInfo = {'debug':True,'port':8084,'threaded':True,'host':'0.0.0.0'} -# if 'app' not in _args: -# _info['app'] = _appInfo -# else: -# _info['app'] = dict(_appInfo,**_args['app']) +class delegate: + def __call__(self,**_args) : + _handler= _args['handler'] #-- module 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] + _data = {} + if hasattr(_pointer,'mimetype') and hasattr(_pointer,'method'): + # + # we constraint the methods given their execution ... + _mimeType = _pointer.mimetype + if _request.method in _pointer.method : + _data = _pointer(request=_request,config=_config) + elif not hasattr(_pointer,'mimetype'): + + _data,_mimeType = _pointer(request=_request,config=_config) + + else: + _mimeType = 'application/octet-stream' + # _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 + # + # We should generate a 500 error in this case with a message ... + # + _mimeType = 'plain/html' + _data = f""" + + """ -# return _info -# def _header(**_args): -# return _isvalid(['logo','title','subtitle'],**_args) -# def _layout(**_args): -# _info = _isvalid(['root','index'],**_args) -# _info['on'] = {"load":{},"error":{}} -# _url = 'qcms.co' -# _overwrite = {"folder1":{"type":"redirect","url":_url},"folder2":{"type":"dialog"},"folder3":{"type":"dialog","url":_url}} -# _info['icons'] = {"comment":"use folder names as keys and fontawesome type as values to add icons to menu"} -# _info["api"] = {"comment":"use keys as uri and function calls as values"} -# _info['map'] = {}, -# _info['order'] = {'menu':[]} -# _info['overwrite'] = _overwrite -# return _info - \ No newline at end of file + return _data,_code,{'Content-Type':_mimeType} diff --git a/cms/apexchart.py b/cms/apexchart.py new file mode 100644 index 0000000..97f93a7 --- /dev/null +++ b/cms/apexchart.py @@ -0,0 +1,185 @@ +import numpy as np +import pandas as pd + +class chart : + @staticmethod + def get(_data,_config) : + # r = {} + # for _key in _config : + # r[_key] = {'about':_config[_key]['about'],'chart':[]} + # _pointers = _config[_key]['apply'] + # _pointers = [_pointers] if type(_pointers) == str else _pointers + # r[_key]['chart'] += [getattr(chart,_name)(_data,_config[_key]) for _name in _pointers if hasattr(chart,_name)] + # return [r] + r = {} + for _key in _config : + _options = _config[_key]['options'] + r[_key] = {'about':_config[_key]['about'],'css':_config[_key]['css'],'charts':[]} + _charts = [] + for _itemOption in _options : + _type = _itemOption['type'] + if hasattr(chart,_type) : + _pointer = getattr(chart,_type) + + _chartOption = _pointer(_data,_itemOption) + _tag = 'options' if _type != 'scalar' else 'html' + if 'title' in _itemOption and _itemOption['type'] != 'scalar' : + _chartOption['title'] = {'text':_itemOption['title'],'align':'center'} + _chartOption['legend'] = {'position':'bottom','itemMargin':{'horizontal':4,'vertical':10}} + + _chartOption['chart']['height'] = 300 + # _chartOption['chart']['height'] = '100%' + # _chartOption['responsive'] = [{'breakpoint':480,'options':{'chart':{'width':300}}}] + _charts.append ({"type":_type,_tag:_chartOption}) + if _charts : + r[_key]['charts'] = _charts + + # for _pointer in _pointers : + # r[_key]['chart'].append(_pointer(_data,_config[_key])) + + # _pointers = [getattr(chart,_name) for _name in _config[_key]['options']['apply'] if hasattr(chart,_name)] + # r[_key] = {'about':_config[_key]['about'],'chart':[]} + # for _pointer in _pointers : + # r[_key]['chart'].append(_pointer(_data,_config[_key])) + + + return r + @staticmethod + def format_digit (value): + if value > 1000000 : + return np.divide(value,1000000).round(2).astype(str) + ' M' + elif value > 1000 : + return np.divide(value,1000).round(2).astype(str)+ ' K' + return value + @staticmethod + def scalar(_data,_config) : + """ + Only focusing on axis.y + """ + _columns = _config['axis']['y'] + if _data.shape[0] > 1 : + _apply = 'sum' if 'apply' not in _config else _config['apply'] + # values = _data[_columns].sum().values.tolist() + values = getattr(_data[_columns],_apply)().values.round(3).tolist() + else: + values = _data[_columns].values.tolist()[0] + + _html = [f'
{chart.format_digit(values[_index])}
{_columns[_index].replace("_"," ") }
' for _index in np.arange(len(values))] + return ' '.join(_html) + @staticmethod + def donut(_data,_config): + options = {"chart":{"type":"donut"}} + _yaxis = _config['axis']['y'] + _apply = 'sum' if 'apply' not in _config else _config['apply'] + # options['series'] = _data[_yaxis].sum().values.tolist() + options['series'] = getattr(_data[_yaxis],_apply)().values.round(3).tolist() + options['labels'] = [_name.replace('_',' ').upper() for _name in _yaxis] + options["dataLabels"]= { + "enabled": False + } + return options + @staticmethod + def column(_data,_config): + if 'apply' in _config : + _fn = _config['apply'] + _yaxis = _config['axis']['y'] + _values = getattr(_data[_yaxis],_fn)().values #.sum() + _data = (pd.DataFrame([dict(zip(_yaxis,_values))])) + + + + options = chart.barStacked(_data,_config) + options['chart'] = {'type':'bar'} + if 'title' in _config : + options['title'] = {'text':_config['title'],'align':'center','style':{'text-transform':'upperCase','fontSize':'18px'}} + pass + + options['stroke'] = {'show':True,'width':2,'colors':['transparent']} + if _data.shape[0] == 1: + options['xaxis']['categories'] = [_name.replace('_',' ').upper() for _name in _config['axis']['x']] + # options['plotOptions'] = {'bar':{'columnWidth':'55%'}} + return options + @staticmethod + def barStacked(_data,_config): + options = {"series":[], "chart": { + "type": 'bar','stacked':True} + } + # options['plotOptions'] = {'bar':{'horizontal':True}} + # options['legend'] = {'position':'bottom'} # {'position':'right','horizontalAlign':'left','offsetX':40} + + _xaxis = _data[_config['axis']['x']].values.tolist() + options["xaxis"]={"categories":_xaxis} + for _col in _config['axis']['y'] : + options['series'] += [{'name':_col.replace('_',' ').upper(), 'data':_data[_col].tolist()}] + return options + @staticmethod + def radialBar (_data,_config) : + + _options = { + "series": _config["axis"]["y"], + "labels": _config["axis"]["x"], + "chart": { + "type": 'radialBar', + "offsetY": -20, + "sparkline": { + "enabled": True + } + }, + # "plotOptions": { + # "radialBar": { + # "startAngle": -90, + # "endAngle": 90, + # "track": { + # "background": "#e7e7e7", + # "strokeWidth": '97%', + # "margin": 5, + # "dropShadow": { + # "enabled": True, + # "top": 2, + # "left": 0, + # "color": '#999', + # "opacity": 1, + # "blur": 2 + # } + # }, + # "dataLabels": { + + # "name": { + # "show": False + # }, + # "value": { + # "offsetY": -2, + # "fontSize": '18px' + # } + # } + # } + # }, + # "grid": { + # "padding": { + # "top":10 + # } + # }, + + + } + return _options + @staticmethod + def barGrouped (_data,_config): + """ + """ + options = {"series":[],"chart":{"type":"bar"},"plotOptions": { + "bar": { + "horizontal": True, + "dataLabels": { + "position": 'top', + }}}, + + } + _yaxis = _config["axis"]["y"] + for _name in _yaxis : + options["series"] += [{'name':_name.replace('_',' ').upper(),"data":_data[_name].tolist()}] + # + # _xaxis + _xaxis = _config['axis']['x'] + options['xaxis'] = {'categories':_data[_xaxis].values.tolist()} + return options diff --git a/cms/disk.py b/cms/disk.py index 65e94d1..1b7efa6 100644 --- a/cms/disk.py +++ b/cms/disk.py @@ -8,6 +8,7 @@ import copy import mistune from mistune import markdown import re +import plugin_ix def folders (_path,_config): """ @@ -143,29 +144,34 @@ def plugins (**_args): key = f'{_context}/{key}' return {key:read} - _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')] - if files: - _uri = [_path,files[0]] - if _context : - _uri = [_context] + _uri - _path = os.sep.join(_uri) - else: - return None - else: - # - # LOG: not a file - return None - #-- We have a file ... - _name = _args['name'] - spec = importlib.util.spec_from_file_location(_name, _path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) + _path = _args['path'] #os.sep.join([_args['root'],'plugin']) + loader = plugin_ix.Loader(file=_path) + if _args['name'] in loader.names() : + _pointer = loader.get(_args['name']) + return _pointer + return None + # if os.path.isdir(_path): + # files = os.listdir(_path) + # if files : + # files = [name for name in files if name.endswith('.py')] + # if files: + # _uri = [_path,files[0]] + # if _context : + # _uri = [_context] + _uri + # _path = os.sep.join(_uri) + # else: + # return None + # else: + # # + # # LOG: not a file + # return None + # #-- We have a file ... + # _name = _args['name'] + # spec = importlib.util.spec_from_file_location(_name, _path) + # module = importlib.util.module_from_spec(spec) + # spec.loader.exec_module(module) - # - # LOG This plugin .... - return getattr(module,_name) if hasattr(module,_name) else None + # # + # # LOG This plugin .... + # return getattr(module,_name) if hasattr(module,_name) else None \ No newline at end of file diff --git a/cms/engine/basic.py b/cms/engine/basic.py index 2d3dee3..1338a41 100644 --- a/cms/engine/basic.py +++ b/cms/engine/basic.py @@ -140,8 +140,9 @@ class Initializer : if 'key' in _source : # _path = _source['key'] - if 'location' in self._config['layout'] : + if 'location' in self._config['layout'] and not os.path.exists(_path): _path = os.sep.join([self._config['layout']['location'],_path]) + if os.path.exists(_path) : f = open(_path) _source['key'] = f.read() diff --git a/cms/engine/plugins/__init__.py b/cms/engine/plugins/__init__.py index 86858f0..de60ce0 100644 --- a/cms/engine/plugins/__init__.py +++ b/cms/engine/plugins/__init__.py @@ -9,42 +9,42 @@ import os # -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) - pass -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']) +# 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) +# pass +# 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 +# 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 +# return getattr(module,_name) if hasattr(module,_name) else None diff --git a/cms/engine/project/__init__.py b/cms/engine/project/__init__.py index 7b1e6ef..efb49a3 100644 --- a/cms/engine/project/__init__.py +++ b/cms/engine/project/__init__.py @@ -49,6 +49,28 @@ def make_folder (projectFolder, webroot): _projectPath = os.sep.join([_path,folder]) if not os.path.exists(_projectPath) : os.makedirs(_projectPath) +def _icode(_path,_root): + """ + This function will generate some default plugins to show the users how plugins work and can be used/written + :path location of the project + """ + _code = """ + import cms + # + # register this in config.plugins: {"demo":["info"]} + + + @cms.plugins(mimetype='application/json') : + def info (**_args): + _request= _args['request'] + _config = _args['config'] + return {"version":_config['system']['version'],'title':_config['layout']['header']['title']} + pass + """ + loc = os.sep.join([_path,_root,'_plugins','demo.py']) + f = open(loc,'w') + f.write(_code) + f.close() def _ilogo (_path): """ This function creates a default logo in a designated folder, after the project folder has been created @@ -143,13 +165,13 @@ def make (**_args) : _config['plugins'] = {} _folder = _args['folder'] _root = _config['layout']['root'] #-- web root folder - make_folder(_folder,_root) - f = open(os.sep.join([_folder,'qcms-manifest.json']),'w') + make_folder(_folder,_root) #-- creating the project folder structure + f = open(os.sep.join([_folder,'qcms-manifest.json']),'w') #-- adding the manifest file f.write( json.dumps(_config)) f.close() - _ilogo(os.sep.join([_folder,_root])) - print ([_folder,_root]) + _ilogo(os.sep.join([_folder,_root])) #-- adding logo _index(os.sep.join([_folder,_root]),_root) - _itheme(_folder,_root) - \ No newline at end of file + _itheme(_folder,_root) #-- adding theme folder + _icode(_folder,_root) #-- adding the plugins sample code + diff --git a/cms/engine/themes/__init__.py b/cms/engine/themes/__init__.py index 63e7527..8af40ee 100644 --- a/cms/engine/themes/__init__.py +++ b/cms/engine/themes/__init__.py @@ -32,7 +32,11 @@ def Get(theme,_url= URL) : """ try: _url = '/'.join([_url,'api','themes','Get']) +f'?theme={theme}' - return requests.get(_url).json() + try: + return requests.get(_url).json() + except Exception as e: + pass + return None except Exception as e: pass return {} diff --git a/cms/index.py b/cms/index.py index 9dce0c1..de5870b 100644 --- a/cms/index.py +++ b/cms/index.py @@ -137,58 +137,10 @@ def _delegate_call(app,key,module,name): _handler = _getHandler(app,key) # print (_handler.config()/) uri = f'api/{module}/{name}' - return Plugin.call(uri=uri,handler=_handler,request=request) - # return _delegate(_handler,module,name) + # return Plugin.call(uri=uri,handler=_handler,request=request) + _delegate = cms.delegate() + return _delegate(uri=uri,handler=_handler,request=request) -def _delegate(_handler,module,name): - global _route - uri = '/'.join(['api',module,name]) - # _args = dict(request.args,**{}) - # _args['config'] = _handler.config() - _plugins = _handler.plugins() - _context = _handler.system()['context'] - if _context : - uri = f'{_context}/{uri}' - _mimeType = 'application/octet-stream' - if uri not in _plugins : - _data = {} - _code = 404 - else: - pointer = _plugins[uri] - # if _args : - # _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()) - - _mimeType = 'application/octet-stream' if not _mimeType else _mimeType - if type(_data) == pd.DataFrame : - _data = _data.to_dict(orient='records') - if type(_data) == list: - _data = json.dumps(_data) - _code = 200 if _data else 500 - return _data,_code,{'Content-Type':_mimeType} - -# @_app.route("/api//" , methods=['POST'],defaults={'app_id':'main','key':None}) -# @_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']) - -# 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) @_app.route('/version') def _version (): @@ -206,7 +158,7 @@ def _reload(key) : _systemKey = None elif 'key' in _system['source'] and _system['source']['key']: _systemKey = _system['source']['key'] - print ([key,_systemKey,_systemKey == key]) + # print ([key,_systemKey,_systemKey == key]) if key and _systemKey and _systemKey == key : _handler.reload() return "",200 @@ -253,18 +205,7 @@ def _POST_CMSPage(app_id,key): _id = _uri.split('/')[-1].split('.')[0] else: _id = request.headers['dom'] - # _args = {'layout':_config['layout']} - # if 'plugins' in _config: - # _args['routes'] = _config['plugins'] - - # _system = _handler.system() #cms.components.get_system(_config) - # # _html = _handler.html(_uri,_id,_args,_system) #cms.components.html(_uri,_id,_args,_system) - # _html = _handler.html(_uri,_id) - # # _system = cms.components.get_system(_config) - # _args['system'] = _handler.system(skip=['source','app']) - # e = Environment(loader=BaseLoader()).from_string(_html) - # _html = e.render(**_args) if 'read?uri=' in _uri or 'download?doc=' in _uri : _uri = _uri.split('=')[1] _args = _route.render(_uri,_id,_getId(app_id,key)) #session.get(app_id,'main')) diff --git a/cms/static/js/dashboard.js b/cms/static/js/dashboard.js index f4c1df9..fd4d47b 100644 --- a/cms/static/js/dashboard.js +++ b/cms/static/js/dashboard.js @@ -179,20 +179,52 @@ if(!qcms){ var qcms = {} } -var _dashboard = function(_context,_uri){ - this._context = _context ; +qcms.dashboard = function(_uri,_id){ this._uri = _uri - - this.get = function (_args){ + this._id = _id + this.get = function (){ var _uri = this._uri ; - if (this._context){ - _uri = this._context + _uri - } var http = HttpClient.instance() http.setHeader('Content-Type','application/json') - http.setData(JSON.stringify(_args)) + //http.setData(JSON.stringify(_args)) + var _render = this.render + var _id = this._id http.post(_uri,function(x){ - if(x.readyState == 4 && x.status == 200){} + if(x.readyState == 4 && x.status == 200){ + var _logs = JSON.parse(x.responseText) + _events = [] + _logs.forEach(_item=>{ + + var _e = _render(_item,_id) + if (_e != null){ + _events.push(_e) + } + }) + _events.forEach(_e=>{ _e.render() }) + } }) } + this.render = function (_item,_id){ + if (! _item.options){ + // + // This is html to be rendered + if(_item.constructor.name == 'Array'){ + _item.forEach(_div =>{$(_id).append(_div) }) + }else{ + + $(_id).append($(_item)) + } + return null + }else { + // + // rendering apexcharts + // + var _divChart = jx.dom.get.instance('DIV') + _divChart.className = 'chart '+_item.type + $(_id).append(_divChart) + var _chart = new ApexCharts(_divChart,_item.options) + return _chart ; + + } + } } diff --git a/cms/static/js/dialog.js b/cms/static/js/dialog.js index c7a5055..3dd652d 100644 --- a/cms/static/js/dialog.js +++ b/cms/static/js/dialog.js @@ -5,6 +5,9 @@ dialog.show = function(_args,_pointer){ // http.setData({title:_title,html:_message},'application/json') var uri = _args.context+'/dialog' http.setHeader('dom',_args.title) + if (_args.uri.match(/=/)){ + _args.uri = _args.uri.split(/=/)[1] + } http.setHeader('uri',_args.uri) http.get(uri,function(x){ $('.jxmodal').remove() diff --git a/cms/static/js/search.js b/cms/static/js/search.js index e69de29..10d33cc 100644 --- a/cms/static/js/search.js +++ b/cms/static/js/search.js @@ -0,0 +1,14 @@ +var Search = function(_searchBoxId,_paneId,_bind){ + var _text = jx.dom.get.value(_searchBoxId) + _regex = new RegExp(_text.toLowerCase()) + _paneId = (_paneId['#'])?_paneId:('#'+_paneId) + $(_paneId).slideUp() + (_paneId).children().each(_index=>{ + _div = $(_paneId).children()[_index] + if (_div._data.match(_regex)){ + $(_div).slideDown() + } + }) + + +} \ No newline at end of file diff --git a/meta/__init__.py b/meta/__init__.py index 9f3f8ef..7fa2050 100644 --- a/meta/__init__.py +++ b/meta/__init__.py @@ -1,5 +1,5 @@ __author__ = "Steve L. Nyemba" -__version__= "2.2.8" +__version__= "2.2.10" __email__ = "steve@the-phi.com" __license__=""" Copyright 2010 - 2024, Steve L. Nyemba, Vanderbilt University Medical Center