Merge pull request 'master' () from master into release

Reviewed-on: 
release
Steve L. Nyemba 3 weeks ago
commit 30aeb10e66

@ -0,0 +1,2 @@
recursive-include cms/templates *
recursive-include cms/static *

@ -0,0 +1,430 @@
#!/usr/bin/env python
import numpy as np
import os
import sys
import json
import importlib
import importlib.util
# from git.repo.base import Repo
import typer
from typing_extensions import Annotated
from typing import Optional
from typing import Tuple
import meta
import uuid
from termcolor import colored
import base64
import io
import cms
from cms import index
from cms.engine.config.structure import Layout, System
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
version {meta.__version__}
{meta.__license__}"""
# 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
"""
#
# 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])
return os.sep.join([manifest,'qcms-manifest.json'])
else:
return manifest
@cli.command(name="info")
def _info():
"""
This function returns metadata information about this program, no parameters are needed
"""
print ()
print ('QCMS',meta.__version__)
print (meta.__author__,meta.__email__)
print ()
@cli.command(name='setup')
# def set_app (host:str="0.0.0.0",context:str="",port:int=8084,debug=True):
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
These parameters are the same used in any flask application
"""
global INVALID_FOLDER
path = get_manifest(manifest)
_config = cms.engine.config.get(path)
if _config :
# _system = _config['system']['app']
_app = _config['system']['app']
_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
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(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())
if not set(['url','token','uid']) - set(_auth.keys()) :
url = _auth['url']
if os.path.exists('qcms-manifest.json') :
_config = cms.engine.config.get()
_config['system']['source'] = path #{'id':'cloud','auth':{'url':url,'uid':uid,'token':token}}
cms.engine.config.write(_config)
title = _config['layout']['header']['title']
_msg = f"""{PASSED} [bold]{_config['system']['layout']['header']['title']}[/bold] : successfully update, good job!
{url}
"""
else:
_msg = INVALID_FOLDER
else:
_msg = """NOT A VALID NEXTCLOUD FILE"""
else:
_msg = INVALID_FOLDER
print (_msg)
@cli.command(name='secure')
# def set_key(path):
def secure(
manifest:Annotated[str,typer.Argument(help="path of the manifest file")],
keyfile:Annotated[str,typer.Argument(help="path of the key file to generate")]):
"""
Create a security key and set it in the manifest (and store in on disk).
The secure key allows for administrative control of the application/portal
"""
keyfile = get_manifest(keyfile)
if not os.path.exists(keyfile):
f = open(keyfile,'w')
f.write(str(uuid.uuid4()))
f.close()
#
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
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} [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}
"""
print (_msg)
def load(**_args):
"""
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')]
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(_name, _path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return getattr(module,_name) if hasattr(module,_name) else None
@cli.command(name='plugins')
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,
pointer:str=typer.Option(default=None,help="pointer is structured as 'filename.function'")
) :
"""
Manage plugins list loaded plugins,
"""
manifest = get_manifest(manifest)
_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 = []
_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('.')
# _fnpointer = plugins.load(_root,file+'.py',fnName)
# _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} [bold]{_config['layout']['header']['title']}[/bold]: registered {pointer}, use the --show option to list loaded plugins"""
else:
_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} [bold]{_config['layout']['header']['title']}[/bold]: unregistered {pointer}, use the --show option to list loaded plugins """
# #
# # We need to write this down !!
if add in [True,False] :
_config['plugins'] = _plugins
cms.engine.config.write(_config,manifest)
# else:
# _msg = f"""{FAILED} [bold]{_config['layout']['header']['title']}[/bold]: no plugins are loaded """
print()
print(_msg)
else:
_msg = f"""{FAILED} No plugin folder could be found in {manifest}"""
print (_msg)
@cli.command (name='create')
def create(folder:Annotated[str,typer.Argument(help="path of the project folder")],
# root:Annotated[str,typer.Argument(help="folder of the content (inside the project folder)")],
index:str=typer.Option(default="index.html",help="index file (markup or html)"), #Annotated[str,typer.Argument(help="index file to load (.html or markup)")]='index.html',
title:str=typer.Option(default="QCMS PROJECT", help="name of the project") , #Annotated[str,typer.Argument(help="title of the project")]='QCMS - TITLE',
subtitle:str=typer.Option(default="designed & built by The Phi Technology",help="tag line about the project (sub-title)"), #Annotated[str,typer.Argument(help="subtitle of the project")]='qcms - subtitle',
footer:str=typer.Option(default="Powered By QCMS",help="text to be placed as footer"), #Annotated[str,typer.Argument(help="text on the footer of the main page")]='Quick Content Management System',
port:int=typer.Option(default=8084, help="port to be used for the app"),
context:str=typer.Option(default="", help="application context if using a proxy server"),
version:str=typer.Option(default=meta.__version__,help="version number") #Annotated[str,typer.Argument(help="version of the site")]='0.1'
):
"""
Create a project folder by performing a git clone (pull the template project)
and adding a configuration file to that will bootup the project
"""
#
# Build the configuration for the project
if not os.path.exists(folder) :
root = 'www/html'
_system = System()
_layout = Layout()
_layout = _layout.build(root=root, order={'menu':[]}, footer = [{"text":term} for term in footer.split(',')],header={'title':title,'subtitle':subtitle} )
_system = System()
_system = _system.build(version=version,port=port, context=context)
_config = dict(_system,**_layout)
print (f"{PASSED} Built Configuration Object")
#
# Setup Project on disk
#
cms.engine.project.make(folder=folder,config=_config)
print (f"""{PASSED} created project at {folder} """)
else:
print ()
print (f"""{FAILED} Could not create project {folder}
The folder is not empty
""")
@cli.command (name='reload')
def reload (
path:Annotated[str,typer.Argument(help="")],
port:int=typer.Option(default=None,help="port of the host to call")
) :
"""
Reload a site/portal given the manifest ...
"""
path = get_manifest(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'])
if not os.path.exists(_spath) :
mpath = path.split(os.sep)[:-1] + _spath.split(os.sep)
_spath = os.sep.join(mpath)
pass
f = open(_spath)
key = f.read()
f.close()
print (key)
_port = port if port else _config['system']['app']['port']
url = f"http://localhost:{_port}/reload"
resp = requests.post(url, headers={"key":key})
if resp.status_code == 200 :
_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}
"""
else:
_msg = f"""{FAILED} no secure key found in manifest to request reload"""
print (_msg)
@cli.command(name="bootup")
def bootup (
manifest:Annotated[str,typer.Argument(help="path of the manifest file")]='qcms-manifest.json',
port:int=typer.Option(default=None, help="port number to serve on (will override configuration)")
):
"""
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'])
manifest = get_manifest(manifest)
index.start(manifest,port)
@cli.command(name='theme')
def handle_theme (
manifest:Annotated[str,typer.Argument(help="path of the manifest file")],
show:bool = typer.Option(default=False,help="print list of themes available"),
name:str = typer.Option(default='default',help='name of the theme to apply')
) :
"""
This function will show the list available themes and can also set a theme in a manifest
"""
manifest = get_manifest(manifest)
_config = cms.engine.config.get(manifest)
_root = os.sep.join( manifest.split(os.sep)[:-1]+[_config['layout']['root']])
if show :
_available = themes.List()
_df = pd.DataFrame({"available":_available})
# if _df.shape[0] > 0 :
if _available :
_installed = themes.installed(_root)
_available.sort()
_marks = []
for _themeName in _available :
_symbol = '\u2713' if _themeName in _installed else '\u2717'
if _config['system']['theme'] == _themeName :
_symbol = '\u2605'
# colored('\u2713', 'green')
# _symbol = _symbol.replace('[ ','').replace(' ]','')
_marks.append( str(_symbol))
_df = pd.DataFrame({"available":_available,"installed":_marks})
# values.sort()
# values = values + np.repeat(f"""{FAILED}""", _df.shape[0] - len(values)).tolist()
# values.sort()
# _df['installed'] = values
else:
_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
#
try:
_object = themes.Get(name)
path = os.sep.join([_root,'_assets','themes',name])
if not os.path.exists(path) :
os.makedirs(path)
_object = _object[name] #-- formatting (...)
for _filename in _object :
css = _object[_filename]
f = open(os.sep.join([path,_filename+'.css']),'w')
f.write(css)
f.close()
#
# Let us update the configuration file ...
#
_config['system']['theme'] = name
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} [[bold]{_config['system']['layout']['header']['title']}[/bold] : operation failed "{str(e)}"
"""
pass
print (_msg)
global SYS_ARGS
if __name__ == '__main__':
cli()

@ -6,134 +6,174 @@ import copy
from jinja2 import Environment, BaseLoader, FileSystemLoader
import importlib
import importlib.util
import json
class components :
@staticmethod
def folders (_path):
_content = os.listdir(_path)
return [_name for _name in _content if os.path.isdir(os.sep.join([_path,_name])) if not _name.startswith('_')]
@staticmethod
def content (_folder) :
if os.path.exists(_folder) :
# return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_folder,_name])} for _name in os.listdir(_folder) if not _name.startswith('_') and os.path.isfile( os.sep.join([_folder,_name]))]
return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_folder,_name])} for _name in os.listdir(_folder) if not _name.startswith('_') and os.path.isfile( os.sep.join([_folder,_name]))]
from . import apexchart
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:
return []
@staticmethod
def menu(_path,_config):
"""
This function will read menu and sub-menu items from disk structure,
The files are loaded will
"""
_items = components.folders(_path)
_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'])
_layout = copy.deepcopy(_config['layout'])
_overwrite = _layout['overwrite'] if 'overwrite' in _layout else {}
#
# content of each menu item
_subItems = [ components.content (os.sep.join([_path,_name]))for _name in _items ]
_object = dict(zip(_items,_subItems))
#-- applying overwrites to the menu items
for _name in _object :
_submenu = _object[_name]
_index = 0
for _item in _submenu :
text = _item['text']
if text in _overwrite :
if 'uri' in _item and 'url' in 'url' in _overwrite[text] :
del _item['uri']
_item = dict(_item,**_overwrite[text])
if 'uri' in _item:
_item['uri'] = _item['uri'].replace(_layout['root'],'')
_submenu[_index] = _item
_index += 1
return _object
@staticmethod
def html(uri,id,_args={}) :
"""
This function reads a given uri and returns the appropriate html document, and applies environment context
# 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
"""
_html = (open(uri)).read()
# @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)
#return ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>'])
_html = ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>'])
appContext = Environment(loader=BaseLoader()).from_string(_html)
#
# If the rendering of the HTML happens here we should plugin custom functions (at the very least)
#
return appContext.render(**_args)
# return _html
@staticmethod
def data (_args):
"""
:store data-store parameters (data-transport, github.com/lnyemba/data-transport)
:query query to be applied against the store (expected data-frame)
"""
_store = _args['store']
reader = transport.factory.instance(**_store)
_queries= copy.deepcopy(_store['query'])
_data = reader.read(**_query)
return _data
@staticmethod
def csv(uri) :
return pd.read(uri).to_html()
@staticmethod
def load_plugin(**_args):
def call(**_args):
"""
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
This function will execute a plugged-in function given
"""
_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:
_path = os.sep.join([_path,files[0]])
else:
return None
# _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:
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)
return getattr(module,_name) if hasattr(module,_name) else None
@staticmethod
def plugins(_config) :
PATH= os.sep.join([_config['layout']['root'],'_plugins'])
_map = {}
if not os.path.exists(PATH) :
return _map
_conf = _config['plugins']
_mimeType = 'application/octet-stream'
try:
_data,_mimeType = _pointer(request=_request,config=_config)
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
#
# We should generate a 500 error in this case with a message ...
#
_mimeType = 'plain/html'
_data = f"""
<script>
</script>
"""
for _key in _conf :
_path = os.sep.join([PATH,_key+".py"])
return _data,_code,{'Content-Type':_mimeType}
pass
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']
for _name in _conf[_key] :
_pointer = components.load_plugin(path=_path,name=_name)
if _pointer :
_uri = "/".join(["api",_key,_name])
_map[_uri] = _pointer
return _map
@staticmethod
def context(_config):
"""
adding custom variables functions to Jinja2, this function should be called after plugins are loaded
"""
_plugins = _config['plugins']
# if not location:
# env = Environment(loader=BaseLoader())
# else:
location = _config['layout']['root']
# env = Environment(loader=FileSystemLoader(location))
env = Environment(loader=BaseLoader())
# env.globals['routes'] = _config['plugins']
return env
_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"""
<script>
//
// I should
_msg = '<div class="border-round" style="display:grid; grid-template-columns:100px auto; gap:8px; align-content:center"><div class="large-text">404</div>,div> Resource NOT found</di><div>'
</script>
"""
return _data,_code,{'Content-Type':_mimeType}

@ -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'<div class="scalar"><div class="value">{chart.format_digit(values[_index])}</div><div class="label">{_columns[_index].replace("_"," ") }</div></div>' 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

@ -0,0 +1,175 @@
"""
Reads from nextcloud
"""
import nextcloud_client as nc
import copy
from mistune import markdown #version 0.8.4
import os
import time
import json
_CLOUDHANDLER = None
def login(path): #(**_args):
f = open(path)
_args = json.loads(f.read())
f.close()
nx = nc.Client(_args['url'])
nx.login(_args['uid'],_args['token'])
time.sleep(1)
return nx
def _format_root_folder (_root):
if _root[0] == '/' :
_root = _root[1:]
if _root[-1] == '/' :
_root = _root[:-1]
return _root.replace('//','/')
def list_files(folder,_config) :
"""
List the content of a folder (html/md) for now
"""
_authfile = _config['system']['source']['auth']
_handler = login(_authfile)
_files = _handler.list(folder,50)
_content = []
for _item in _files :
if _item.file_type == 'file' and _item.get_content_type() in ['text/markdown','text/html'] :
_uri = '/'.join(_item.path.split('/')[2:])
_uri = _item.path
# _content.append({'text':_item.name.split('.')[0],'uri':_uri})
_content.append(_item.name)
return _content
def content(_args):
"""
:url
:uid
:token
:folder
"""
# global _CLOUDHANDLER
# if not _CLOUDHANDLER :
# _handler = nc.Client(_args['url'])
# _handler.login(_args['uid'],_args['token'])
# _CLOUDHANDLER = _handler
_handler = login(_args['auth'])
_root = _args['folder']
if _root.startswith('/') :
_root = _root[1:]
if _root.endswith('/') :
_root = _root[:-1]
_files = _handler.list(_root,30)
_menu = {} #[_args['folder']] + [_item for _item in _files if _item.file_type == 'dir' and _item.name[0] not in ['.','_']]
_menu = {} #dict.fromkeys(_menu,[])
for _item in _files :
_folder = _item.path.split(_item.name)[0].strip()
_folder = _folder.replace(_root,'').replace('//','')
#
# The following lines are intended to prevent an irradict recursive read of a folder content
# We want to keep things simple as we build the menu
#
if len (_folder.split('/')) > 2:
continue
else:
_folder = _folder.replace('/','')
if _item.name[0] in ['.','_'] or _folder == '':
continue ;
if _item.file_type == 'file' and _item.get_content_type() in ['text/markdown','text/html'] :
# _folder = _item.path.split(_item.name)[0].strip()
# _folder = _folder.replace(_root,'').replace('//','')
if _folder == '' :
_folder = str(_root)
_folder = _folder.replace('/' ,' ').strip()
if _folder not in _menu :
_menu [_folder] = []
uri = '/'.join(_item.path.split('/')[2:])
uri = _item.path
_menu[_folder].append({'text':_item.name.split('.')[0],'uri':uri})
#
# clean up the content ...
_keys = [_name for _name in _menu.keys() if _name.startswith('_')]
[_menu.pop(_name) for _name in _keys]
_handler.logout()
return _menu
def build(_config):
"""
The function will build a menu based on a folder structure in nextcloud
:_args authentication arguments for nextcloud
:_config configuration for the cms
"""
_args = {'auth':copy.deepcopy(_config['system']['source']['auth'])}
_args['folder'] = _config['layout']['root']
# update(_config)
_menu = content(_args)
return _menu
def html (uri,_config) :
# global _CLOUDHANDLER
# _handler = _CLOUDHANDLER
_handler = login(_config['system']['source']['auth'])
_root = _format_root_folder(_config['layout']['root'])
uri = _format_root_folder (uri)
_context = _config['system']['context']
_prefix = '/'.join (uri.split('/')[:-1])
_link = '/'.join(['api/cloud/download?doc='+_prefix,'.attachments.'])
if _context :
_link = f'{_context}/{_link}'
# _link = '/'.join(['api/cloud/download?doc='+_prefix,'_images'])
_html = _handler.get_file_contents(uri).decode('utf-8')#.replace('.attachments.', copy.deepcopy(_link))
# print ([uri,uri[-2:] ,uri[-2:] in ['md','MD','markdown']])
_handler.logout()
# if uri.endswith('.md'):
if f"{_root}{os.sep}" in _html :
if not _context :
_html = _html.replace(_root,('api/cloud/download?doc='+_root)).replace('.attachments.', copy.deepcopy(_link))
else:
_html = _html.replace(_root,(f'{_context}api/cloud/download?doc='+_root)).replace('.attachments.', copy.deepcopy(_link))
# _html = _html.replace('<br />','')
return markdown(_html).replace("&quot;",'"').replace("&lt;","<").replace("&gt;",">") if uri[-2:] in ['md','MD','Md','mD'] else _html
# def update (_config):
# """
# This function updates the configuration provided by loading default plugins
# """
# if 'plugins' not in _config :
# _config['plugins'] = {}
# _config['plugins'] = plugins ()
# return _config
def download(**_args):
_auth = _args['config']['system']['source']['auth']
_request = _args['request']
_handler = login(_auth)
if _request.args['doc'][-2:] in ['md','ht']:
_stream = html(_request.args['doc'],_args['config'])
else:
_stream = _handler.get_file_contents(_request.args['doc'])
_handler.logout()
return _stream
pass
def _format (uri,_config) :
"""
This function does nothing but is used to satisfy the demands of a design pattern
@TODO: revisit the design pattern
"""
return uri
def plugins (_context):
"""
This function publishes the plugins associated with this module
"""
key = 'api/cloud/download'
if _context :
key = f'{_context}/{key}'
return {key:download}

@ -0,0 +1,177 @@
"""
This file pulls the content from the disk
"""
import os
import importlib
import importlib.util
import copy
import mistune
from mistune import markdown
import re
import plugin_ix
def folders (_path,_config):
"""
This function reads the content of a folder (no depth, it must be simple)
"""
_content = os.listdir(_path)
return [_name for _name in _content if os.path.isdir(os.sep.join([_path,_name])) if not _name.startswith('_')]
def content(_folder,_config,keep=[]):
"""
:content of the folder
"""
_layout = _config['layout']
if 'location' in _layout :
_uri = os.sep.join([_layout['root'] ,_folder.split(os.sep)[-1]])
_path = os.sep.join([_layout['root'],_folder.split(os.sep)[-1]])
else:
_path = _folder
if os.path.exists(_folder) :
_menuItems = list_files(_folder,_config) #os.listdir(_folder)
# return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_folder,_name])} for _name in os.listdir(_folder) if not _name.startswith('_') and os.path.isfile( os.sep.join([_folder,_name]))]
return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_path,_name])} for _name in os.listdir(_folder) if not _name[0] in ['.','_'] and os.path.isfile( os.sep.join([_folder,_name])) and _name.split('.')[-1] in ['html','md']]
else:
return []
def list_files(_folder,_config, keep=[]):
return [name for name in os.listdir(_folder) if name[0] not in ['.','_']]
def build (_config, keep=[]): #(_path,_content):
"""
building the menu for the site given the content is on disk
:path path of the files on disk
:config configuration associated with the
"""
_path = _config['layout']['root']
# if 'location' in _config['layout'] :
# _path = _config['layout']['location']
_path = _realpath(_path,_config)
# print (_path)
_items = folders(_path,_config)
_subItems = [ content (os.sep.join([_path,_name]),_config)for _name in _items ]
_r = {}
for _name in _items :
_index = _items.index(_name)
if _name.startswith('_') or len(_subItems[_index]) == 0:
continue
# print ([_name,_subItems[_index]])
if _name not in _r :
_r[_name] = []
_r[_name] += _subItems[_index]
# _r = [_r[_key] for _key in _r if len(_r[_key]) > 0]
return _r
# return dict.fromkeys(_items,_subItems)
def _realpath (uri,_config) :
_layout = _config['layout']
_uri = copy.copy(uri)
if 'location' in _layout :
_uri = os.sep.join([_layout['location'],_uri])
return _uri
def _format (uri,_config):
_layout = _config['layout']
if 'location' in _layout :
return 'api/disk/read?uri='+uri
return uri
def read (**_args):
"""
This will read binary files from disk, and allow the location or not to be read
@TODO: add permissions otherwise there can be disk-wide reads
"""
request = _args['request']
_layout = _args['config']['layout']
_uri = request.args['uri'] # if 'location' in _layout :
# _uri = os.sep.join([_layout['location'],_uri])
_uri = _realpath(_uri, _args['config'])
_mimeType = 'text/plain'
if os.path.exists(_uri):
f = open(_uri,mode='rb')
_stream = f.read()
f.close()
#
# Inferring the type of the data to be returned
_mimeType = 'application/octet-stream'
_extension = _uri.split('.')[-1]
if _extension in ['css','js','csv','html'] :
_mimeType = f'text/{_extension}'
elif _extension in ['png','jpg','jpeg'] :
_mimeType = f'image/{_extension}'
return _stream, _mimeType
return None,_mimeType
def exists(**_args):
_path = _realpath(_args['uri'],_args['config'])
# _layout = _args['config']['layout']
# if 'location' in _layout :
# _path = os.sep.join([_layout['location'],_path])
return os.path.exists(_path)
def html(_uri,_config) :
# _html = (open(uri)).read()
_path = _realpath(_uri,_config)
_context = str(_config['system']['context'])
# if '/' in _context :
# _context = _context.split('/')[-1]
_html = ( open(_path)).read()
_layout = _config['layout']
if 'location' in _layout :
if not _config :
_api = os.sep.join(['api/disk/read?uri=',_layout['root']])
else:
_api = os.sep.join([f'{_context}/api/disk/read?uri=',_layout['root']])
if f"{_layout['root']}{os.sep}" in _html :
_html = _html.replace(f"{_layout['root']}",_api)
_html = mistune.html(_html).replace("&quot;",'"').replace("&lt;","<").replace("&gt;",">") if _uri[-2:] in ['md','MD','Md','mD'] else _html
return _html
def plugins (**_args):
"""
This function will load plugins from disk given where they come from
:path path of the files
:name name of the module
"""
_context = _args['context']
if 'path' not in _args :
key = 'api/disk/read'
if _context :
key = f'{_context}/{key}'
return {key:read}
_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

@ -0,0 +1,385 @@
# import json
# from genericpath import isdir
# import os
# import pandas as pd
# import transport
# import copy
# from jinja2 import Environment, BaseLoader, FileSystemLoader
# import importlib
# import importlib.util
from cms import disk, cloud
from . import basic
# class Loader :
# """
# This class is designed to exclusively load configuration from disk into an object
# :path path to the configuraiton file
# :location original location (caller)
# """
# def __init__(self,**_args):
# self._path = _args['path']
# self._original_location = None if 'location' not in _args else _args['location']
# self._location = None
# self._caller = None if 'caller' not in _args else _args['caller']
# print ([' *** ', self._caller])
# self._menu = {}
# self._plugins={}
# self.load()
# def load(self):
# """
# This function will load menu (overwrite) and plugins
# """
# self.init_config()
# self.init_menu()
# self.init_plugins()
# def init_config(self):
# """
# Initialize & loading configuration from disk
# """
# f = open (self._path)
# self._config = json.loads(f.read())
# if self._caller :
# self._location = self._original_location.split(os.sep) # needed for plugin loading
# self._location = os.sep.join(self._location[:-1])
# self._config['system']['portal'] = self._caller != None
# #
# # let's see if we have a location for a key (i.e security key) in the configuration
# #
# self.update_config()
# # _system = self._config['system']
# # if 'source' in _system and 'key' in _system['source'] :
# # _path = _system['source']['key']
# # if os.path.exists(_path):
# # f = open(_path)
# # _system['source']['key'] = f.read()
# # f.close()
# # self._system = _system
# # self._config['system'] = _system
# def update_config(self):
# """
# We are going to update the configuration (source.key, layout.root)
# """
# _system = self._config['system']
# #
# # updating security-key that allows the application to update on-demand
# if 'source' in _system and 'key' in _system['source'] :
# _path = _system['source']['key']
# if os.path.exists(_path):
# f = open(_path)
# _system['source']['key'] = f.read()
# f.close()
# self._system = _system
# self._config['system'] = _system
# _layout = self._config['layout']
# #
# # update root so that the app can be launched from anywhere
# # This would help reduce the footprint of the app/framework
# _path = os.sep.join(self._path.split(os.sep)[:-1])
# _p = 'source' not in _system
# _q = 'source' in _system and _system['source']['id'] != 'cloud'
# _r = os.path.exists(_layout['root'])
# if not _r and (_p or _q) :
# #
# # If we did running this app from installed framework (this should not apply to dependent apps)
# #
# _root = os.sep.join([_path,_layout['root']])
# self._config['layout']['root'] = _root
# self._config['layout']['root_prefix'] = _root
# def init_menu(self):
# """
# This function will read menu and sub-menu items from disk structure,
# The files are loaded will
# """
# _config = self._config
# if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' :
# _sourceHandler = cloud
# else:
# _sourceHandler = disk
# _object = _sourceHandler.build(_config)
# #
# # After building the site's menu, let us add the one from 3rd party apps
# #
# _layout = copy.deepcopy(_config['layout'])
# _overwrite = _layout['overwrite'] if 'overwrite' in _layout else {}
# #
# # @TODO: Find a way to translate rename/replace keys of the _object (menu) here
# #
# #-- applying overwrites to the menu items
# for _name in _object :
# _submenu = _object[_name]
# _index = 0
# for _item in _submenu :
# text = _item['text'].strip()
# if text in _overwrite :
# if 'uri' in _item and 'url' in 'url' in _overwrite[text] :
# del _item['uri']
# _item = dict(_item,**_overwrite[text])
# if 'uri' in _item and 'type' in _item and _item['type'] != 'open':
# _item['uri'] = _item['uri'].replace(_layout['root'],'')
# _submenu[_index] = _item
# _index += 1
# self.init_apps(_object)
# self._menu = _object
# self._order()
# def init_apps (self,_menu):
# """
# Insuring that the apps are loaded into the menu with an approriate label
# """
# _system = self._config['system']
# _context = _system['context']
# if 'routes' in _system :
# # _items = []
# _overwrite = {} if 'overwrite' not in self._config['layout'] else self._config['layout']['overwrite']
# for _text in _system['routes'] :
# _item = _system['routes'][_text]
# if 'menu' not in _item :
# continue
# uri = f'{_context}/{_text}'
# # _items.append ({"text":_text,'uri':uri,'type':'open'})
# _label = _item['menu']
# if _label not in _menu :
# _menu [_label] = []
# _menu[_label].append ({"text":_text,'uri':uri,'type':'open'})
# # _overwrite[_text] = {'text': _text.replace('-',' ').replace('_',' '),'uri':uri,'type':'open'}
# # _menu['products'] = _items
# #
# # given that the menu items assumes redirecting to a page ...
# # This is not the case
# #
# # self._config['overwrite'] = _overwrite
# else:
# pass
# pass
# def _order (self):
# _config = self._config
# if 'order' in _config['layout'] and 'menu' in _config['layout']['order']:
# _sortedmenu = {}
# _menu = self._menu
# for _name in _config['layout']['order']['menu'] :
# if _name in _menu :
# _sortedmenu[_name] = _menu[_name]
# _menu = _sortedmenu if _sortedmenu else _menu
# #
# # If there are missing items in the sorting
# _missing = list(set(self._menu.keys()) - set(_sortedmenu))
# if _missing :
# for _name in _missing :
# _menu[_name] = self._menu[_name]
# _config['layout']['menu'] = _menu #cms.components.menu(_config)
# self._menu = _menu
# self._config = _config
# def init_plugins(self) :
# """
# This function looks for plugins in the folder on disk (no cloud support) and attempts to load them
# """
# _config = self._config
# PATH= os.sep.join([_config['layout']['root'],'_plugins'])
# if not os.path.exists(PATH) :
# #
# # we need to determin if there's an existing
# PATH = os.sep.join(self._path.split(os.sep)[:-1]+ [PATH] )
# if not os.path.exists(PATH) and self._location and os.path.exists(self._location) :
# #
# # overriding the location of plugins ...
# PATH = os.sep.join([self._location, _config['layout']['root'],'_plugins'])
# _map = {}
# # if not os.path.exists(PATH) :
# # return _map
# if 'plugins' not in _config :
# _config['plugins'] = {}
# _conf = _config['plugins']
# for _key in _conf :
# _path = os.sep.join([PATH,_key+".py"])
# if not os.path.exists(_path):
# continue
# for _name in _conf[_key] :
# _pointer = self._load_plugin(path=_path,name=_name)
# if _pointer :
# _uri = "/".join(["api",_key,_name])
# _map[_uri] = _pointer
# #
# # We are adding some source specific plugins to the user-defined plugins
# # This is intended to have out-of the box plugins...
# #
# if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' :
# _plugins = cloud.plugins()
# else:
# _plugins = disk.plugins()
# #
# # If there are any plugins found, we should load them and use them
# if _plugins :
# _map = dict(_map,**_plugins)
# else:
# pass
# self._plugins = _map
# self._config['plugins'] = self._plugins
# def _load_plugin(self,**_args):
# """
# 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')]
# 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(_name, _path)
# module = importlib.util.module_from_spec(spec)
# spec.loader.exec_module(module)
# return getattr(module,_name) if hasattr(module,_name) else None
# class Getter (Loader):
# def __init__(self,**_args):
# super().__init__(**_args)
# def load(self):
# super().load()
# _system = self.system()
# _logo = _system['logo']
# if 'source' in _system and 'id' in _system['source'] and (_system['source']['id'] == 'cloud'):
# _icon = f'/api/cloud/download?doc=/{_logo}'
# _system['icon'] = _icon
# else:
# _root = self._config['layout']['root']
# _icon = os.sep.join([_root,_logo])
# _system['icon'] = _logo
# self._config['system'] = _system
# if self._caller :
# _system['caller'] = {'icon': self._caller.system()['icon']}
# def html(self,uri,id,_args={},_system={}) :
# """
# This function reads a given uri and returns the appropriate html document, and applies environment context
# """
# _system = self._config['system']
# if 'source' in _system and _system['source']['id'] == 'cloud':
# _html = cloud.html(uri,dict(_args,**{'system':_system}))
# else:
# _html = disk.html(uri,self.layout())
# # _html = (open(uri)).read()
# #return ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>'])
# _html = ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>'])
# appContext = Environment(loader=BaseLoader()).from_string(_html)
# _args['system'] = _system
# #
# # If the rendering of the HTML happens here we should plugin custom functions (at the very least)
# #
# return appContext.render(**_args)
# # return _html
# def data (self,_args):
# """
# :store data-store parameters (data-transport, github.com/lnyemba/data-transport)
# :query query to be applied against the store (expected data-frame)
# """
# _store = _args['store']
# reader = transport.factory.instance(**_store)
# _queries= copy.deepcopy(_store['query'])
# _data = reader.read(**_queries)
# return _data
# def csv(self,uri) :
# return pd.read(uri).to_html()
# return _map
# def menu(self):
# return self._config['menu']
# def plugins(self):
# return copy.deepcopy(self._plugins) if 'plugins' in self._config else {}
# def context(self):
# """
# adding custom variables functions to Jinja2, this function should be called after plugins are loaded
# """
# _plugins = self.plugins()
# # if not location:
# # env = Environment(loader=BaseLoader())
# # else:
# location = self._config['layout']['root']
# # env = Environment(loader=FileSystemLoader(location))
# env = Environment(loader=BaseLoader())
# # env.globals['routes'] = _config['plugins']
# return env
# def config(self):
# return copy.deepcopy(self._config)
# def system(self,skip=[]):
# """
# :skip keys to ignore in the object ...
# """
# _data = copy.deepcopy(self._config['system'])
# _system = {}
# if skip and _system:
# for key in _data.keys() :
# if key not in skip :
# _system[key] = _data[key]
# else:
# _system= _data
# return _system
# def layout(self):
# return self._config['layout']
# def get_app(self):
# return self._config['system']['app']
# class Router :
# def __init__(self,**_args) :
# # _app = Getter (path = path)
# _app = Getter (**_args)
# self._id = 'main'
# # _app.load()
# self._apps = {}
# _system = _app.system()
# if 'routes' in _system :
# _system = _system['routes']
# for _name in _system :
# _path = _system[_name]['path']
# self._apps[_name] = Getter(path=_path,caller=_app,location=_path)
# self._apps['main'] = _app
# def set(self,_id):
# self._id = _id
# def get(self):
# return self._apps['main'] if self._id not in self._apps else self._apps[self._id]
# def get_main(self):
# return self._apps['main']

@ -0,0 +1,471 @@
import json
import os
import io
import copy
from cms import disk, cloud
from jinja2 import Environment, BaseLoader, FileSystemLoader
# import importlib
# import importlib.util
"""
There are four classes at play here:
[ Initializer ] <|-- [ Module ] <|-- [ MicroService ] <--<>[ CMS ]
"""
class Initializer :
"""
This class handles initialization of all sorts associated with "cms engine"
:path
:location
:shared
"""
def __init__(self,**_args):
self._config = {'system':{},'layout':{},'plugins':{}}
# self._shared = False if not 'shared' in _args else _args['shared']
self._location= _args['location'] if 'location' in _args else None
self._menu = {}
# _source = self._config ['system']['source'] if 'source' in self._config['system'] else {}
# self._ISCLOUD = 'source' in self._config['system'] and self._config['system']['source']['id'] == 'cloud'
# print ([self._ISCLOUD,self._config['system'].keys()])
self._ISCLOUD = False
self._caller = None if 'caller' not in _args else _args['caller']
self._args = _args
# if 'context' in _args :
# self._config
self.reload() #-- this is an initial load of various components
#
# @TODO:
# Each module should submit it's routers to the parent, and adjust the references given the callers
#
def reload(self):
self._iconfig(**self._args)
self._uconfig(**self._args)
self._isource()
self._imenu()
self._iplugins()
self._iroutes ()
# self._ISCLOUD = 'source' in self._config['system'] and self._config['system']['source']['id'] == 'cloud'
def _handler (self):
"""
This function returns the appropriate handler to the calling code, The handler enables read/write from a location
"""
if self._ISCLOUD: #'source' in self._config['system'] and self._config['system']['source']['id'] == 'cloud' :
return cloud
else:
return disk
def _iroutes (self):
"""
Initializing routes to be submitted to the CMS Handler
The routes must be able to :
1. submit an object (dependency to the cms)
2. submit with the object a route associated
The CMS handler will resolve dependencies/redundancies
"""
pass
def _imenu(self,**_args) :
pass
def _iplugins(self,**_args) :
"""
Initialize plugins from disk (always)
:plugins
"""
_config = self._config
PATH= os.sep.join([_config['layout']['root'],'_plugins'])
if not os.path.exists(PATH) and self._location and os.path.exists(self._location) :
#
# overriding the location of plugins ...
if os.path.isfile(self._location) :
_location = os.sep.join(self._location.split(os.sep)[:-1])
else:
_location = self._location
PATH = os.sep.join([_location, _config['layout']['root'],'_plugins'])
_context = _config['system']['context']
_map = {}
# if not os.path.exists(PATH) :
# return _map
if 'plugins' not in _config :
_config['plugins'] = {}
_conf = _config['plugins']
for _key in _conf :
_path = os.sep.join([PATH,_key+".py"])
if not os.path.exists(_path):
continue
for _name in _conf[_key] :
_pointer = disk.plugins(path=_path,name=_name,context=_context)
if _pointer :
_uri = "/".join(["api",_key,_name])
if _context :
_uri = f'{_context}/{_uri}'
_map[_uri] = _pointer
#
# We are adding some source specific plugins to the user-defined plugins
# This is intended to have out-of the box plugins...
#
if self._ISCLOUD :
_plugins = cloud.plugins(_context)
else:
_plugins = disk.plugins(context=_context)
#
# If there are any plugins found, we should load them and use them
if _plugins :
_map = dict(_map,**_plugins)
else:
pass
self._plugins = _map
self._config['plugins'] = self._plugins
def _isource (self):
"""
Initializing the source of the data, so we can read/write load from anywhere
"""
if 'source' not in self._config['system'] :
return
#
#
self._ISCLOUD = 'source' in self._config['system'] and self._config['system']['source']['id'] == 'cloud'
_source = self._config['system']['source']
if 'key' in _source :
#
_path = _source['key']
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()
f.close()
self._config['system']['source'] = _source
def _ilayout(self,**_args):
"""
Initialization of the layout section (should only happen if ) being called via framework
:path path to the dependent apps
"""
_layout = self._config['layout']
_path = os.sep.join(_args['path'].split(os.sep)[:-1])
#
# find the new root and the one associated with the dependent app
#
pass
def _imenu(self,**_args):
_gshandler = self._handler()
_object = _gshandler.build(self._config) #-- this will build the menu
#
# post-processing menu, overwrite items and behaviors
#
_layout = copy.deepcopy(self._config['layout'])
_overwrite = _layout['overwrite'] if 'overwrite' in _layout else {}
_context = self.system()['context']
for _name in _object :
_submenu = _object[_name]
_index = 0
for _item in _submenu :
text = _item['text'].strip()
if text in _overwrite :
if 'uri' in _item and 'url' in _overwrite[text] :
del _item['uri']
_item = dict(_item,**_overwrite[text])
if 'uri' in _item and 'type' in _item and _item['type'] != 'open':
_item['uri'] = _item['uri'] #.replace(_layout['root'],'')
# _item['uri'] = _gshandler._format(_item['uri'],self._config)
_submenu[_index] = _item
_index += 1
#
# updating menu _items as it relates to apps, configuration and the order in which they appear
#
_layout['menu'] = _object
self._menu = _object
self._config['layout'] = _layout
self._iapps()
self._iorder()
pass
def _iorder (self):
_config = self._config
if 'order' in _config['layout'] and 'menu' in _config['layout']['order']:
_sortedmenu = {}
_menu = self._menu
for _name in _config['layout']['order']['menu'] :
if _name in _menu :
_sortedmenu[_name] = _menu[_name]
_menu = _sortedmenu if _sortedmenu else _menu
#
# If there are missing items in the sorting
_missing = list(set(self._menu.keys()) - set(_sortedmenu))
if _missing :
for _name in _missing :
_menu[_name] = self._menu[_name]
_config['layout']['menu'] = _menu #cms.components.menu(_config)
self._menu = _menu
self._config = _config
def _iapps (self):
"""
Initializing dependent applications into a menu area if need be
"""
_layout = self._config['layout']
_menu = _layout['menu'] if 'menu' in _layout else {}
_system = self._config['system']
_context= _system['context']
if 'routes' in _system :
# _items = []
_overwrite = {} if 'overwrite' not in self._config['layout'] else self._config['layout']['overwrite']
for _text in _system['routes'] :
_item = _system['routes'][_text]
if 'menu' not in _item :
continue
uri = f'{_context}/{_text}'
# _items.append ({"text":_text,'uri':uri,'type':'open'})
_label = _item['menu']
if _label not in _menu :
_menu [_label] = []
_menu[_label].append ({"text":_text,'uri':uri,'type':'open'})
#
# update the menu items and the configuration
#
_keys = list(_menu.keys())
# for _key in _keys :
# if len(_menu[_key]) == 0 :
# del _menu[_key]
# #
# # doing some house-keeping work here to make sure the front-end gets clean data
# #
_layout['menu'] = _menu
self._config['layout'] = _layout
def _ilogo(self):
_gshandler = self._handler()
pass
def _iconfig(self,**_args):
"""
Implement this in a base class
:path or uri
"""
raise Exception ("Configuration Initialization is NOT implemented")
def _uconfig(self,**_args):
"""
This file will update the configuration provided the CMS is run in shared mode (framework level)
"""
if not self._location :
return ;
_path = os.sep.join(self._location.split(os.sep)[:-1])
_layout = self._config['layout']
_oroot = _layout['root']
_orw = _layout['overwrite']
_index = _layout['index']
_newpath = os.sep.join([_path,_oroot])
self._config['system']['portal'] = self._caller != None
_context = self.system()['context']
if self._caller :
#
_callerContext = self._caller.system()['context']
if not self._config['system']['context'] :
self._config['system']['context'] = _callerContext
self._config['system']['caller'] = {'icon': '/main'+self._caller.system()['icon'].replace(_callerContext,'')}
_context = '/'.join([_callerContext,_context]) if _callerContext != '' else _context
if os.path.exists(_newpath) and not self._ISCLOUD:
#
# LOG: rewrite due to the mode in which the site is being run
#
_api = f'{_context}/api/disk/read?uri='+_oroot
_stream = json.dumps(self._config)
_stream = _stream.replace(_oroot,_api)
# self._config = json.loads(_stream)
self._config['layout']['root'] = _oroot
# self._config['layout']['overwrite'] = _orw
#
# We need to update the logo/icon
_logo = self._config['system']['logo']
if self._ISCLOUD:
_icon = f'{_context}/api/cloud/download?doc=/{_logo}'
else:
_icon = f'{_context}/api/disk/read?uri={_logo}'
# if disk.exists(uri=_logo,config=self._config):
# _icon = _logo
_logo = _icon
if self._location :
self._config['layout']['location'] = _path
self._config['system']['icon'] = _icon
self._config['system']['logo'] = _logo
# self.set('layout.root',os.sep.join([_path,_oroot]))
pass
class Module (Initializer):
"""
This is a basic structure for an application working in either portal or app mode,
"""
def __init__(self,**_args):
super().__init__(**_args)
pass
def _iconfig(self, **_args):
"""
initialization of the configuration file i.e loading the files and having a baseline workable structure
:path|stream path of the configuration file
or stream of JSON configuration file
"""
if 'path' in _args :
f = open(_args['path'])
self._config = json.loads(f.read())
f.close()
elif 'stream' in _args :
_stream = _args['stream']
if type(_stream) == 'str' :
self._config = json.loads(_stream)
elif type(_stream) == io.StringIO :
self._config = json.loads( _stream.read())
self._ISCLOUD = 'source' in self._config['system'] and self._config['system']['source'] and self._config['system']['source']['id'] == 'cloud'
if self._caller :
self._config['system']['parentContext'] = self._caller.system()['context']
else:
self._config['system']['parentContext'] = self._config['system']['context']
if 'context' in _args :
self._config['system']['context'] = _args['context']
if '/' in self._config['system']['context'] :
self._config['system']['context'] = self._config['system']['context'].split('/')[-1]
#
#
# self._name = self._config['system']['name'] if 'name' in self._config['system'] else _args['name']
def system (self,skip=[]):
"""
This function returns system attributes without specific components
"""
_data = copy.deepcopy(self._config['system'])
exclude = skip
_system = {}
if exclude and _system:
for key in _data.keys() :
if key not in exclude :
_system[key] = _data[key]
else:
_system= _data
return _system
def layout (self):
return copy.copy(self._config['layout'])
def plugins (self):
return copy.copy(self._config['plugins'])
def config (self):
return copy.copy(self._config)
def app(self):
_system = self.system()
return _system['app']
def set(self,key,value):
"""
This function will update/set an attribute with a given value
:key
"""
_keys = key.split('.')
_found = 0
if _keys[0] in self._config :
_object = self._config[_keys[0]]
for _akey in _object.keys() :
if _akey == _keys[-1] :
_object[_akey] = value
_found = 1
break
#
#
return _found
#
class MicroService (Module):
"""
This is a CMS MicroService class that is capable of initializing a site and exposing Module functions
"""
def __init__(self,**_args):
super().__init__(**_args)
def format(_content,mimetype):
pass
def html (self,uri, id) :
_system = self.system()
_gshandler = self._handler()
#
#@TODO:
# The uri here must be properly formatted, We need to define the conditions for this
#
_html = _gshandler.html(uri,self._config)
return " ".join([f'<div id="{id}" > ',_html, '</div>'])
def context(self):
return Environment(loader=BaseLoader())
def data (self,**_args):
request = _args['request']
def icon (self):
_handler = self._handler()
_uri = self.system()['icon']
return _handler.read(uri=_uri,config=self._config)
class CMS:
"""
This class aggregates microservices and allows the application of a given service (site/app)
"""
def __init__(self,**_args) :
# _app = Getter (path = path)
# _void = MicroService()
_app = MicroService (**_args)
self._id = 'main'
# _app.load()
self._apps = {}
_system = _app.system()
if 'routes' in _system :
_system = _system['routes']
for _name in _system :
_path = _system[_name]['path']
self._apps[_name] = MicroService(context=_name,path=_path,caller=_app,location=_path)
self._apps['main'] = _app
#
# The following are just a simple delegation pattern (it makes the calling code simpler)
#
def config (self):
return self.get().config()
def render(self,_uri,_id,_appid):
# _handler = self.get()
_handler = self._apps[_appid]
_config = _handler.config()
_args = {'layout':_handler.layout()}
if 'plugins' in _config:
_args['routes'] = _config['plugins']
_html = _handler.html(_uri,_id)
_args['system'] = _handler.system(skip=['source','app'])
e = Environment(loader=BaseLoader()).from_string(_html)
_args[_id] = str(e.render(**_args)) #,_args
return _args
def set(self,_id):
self._id = _id
def get(self):
return self._apps['main'] if self._id not in self._apps else self._apps[self._id]
def get_main(self):
return self._apps['main']

@ -0,0 +1,28 @@
"""
This file handles all things configuration i.e even the parts of the configuration we are interested in
"""
import os
import json
def get (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, path):
f = open(path,'w')
f.write( json.dumps(_config)) ;
f.close()

@ -0,0 +1,77 @@
"""
This file handles the structure (strict) of the configuration file,
Not all elements are programmatically editable (for now)
"""
import meta
class Section :
def build (self,**_args):
_data = {}
for _attr in dir(self):
if not _attr.startswith('_') and _attr not in ['build','update']:
_kwargs = _args if _attr not in _args or type(_args[_attr]) !=dict else _args[_attr]
_data[_attr] = getattr(self,_attr)(**_kwargs)
_name = type (self).__name__.lower()
return {_name:_data }
def update(self,_default,**_args) :
for _attr in _default.keys():
if _attr in _args :
_default[_attr] = _args[_attr]
return _default
class System (Section) :
def logo(self,**_args):
return 'www/html/_assets/images/logo.png' if 'logo' not in _args else _args['logo']
def theme (self,**_args):
"""
setting the theme given the name of the theme
:name name of the theme to set (optional)
"""
return 'default' if 'name' not in _args else _args['name']
def version(self,**_args):
return meta.__version__ if 'version' not in _args else _args['version']
def context (self,**_args):
return "" if 'context' not in _args else _args['context']
def app (self,**_args):
_data = {'debug':True,'port':8084,'threaded':True,'host':'0.0.0.0'}
return self.update(_data,**_args)
def source(self,**_args):
_data = {'id':'disk'}
if set(['key','auth']) & set(_args.keys()):
#
# key: reboot the app & auth: for cloud access
#
for _attr in ['key','auth'] :
if _attr in _args:
_data[_attr] = _args[_attr]
_data = self.update(_data,**_args)
return _data
class Layout (Section):
def index (self,**_args):
return "index.html" if 'index' not in _args else _args['index']
def root(self,**_args):
return 'wwww/html' if 'root' not in _args else _args['root']
def footer(self,**_args):
return [{'text':'Powered by QCMS'}] if 'footer' not in _args else _args['footer']
def on(self,**_args):
return {'load':{}} if 'on' not in _args else _args['on']
def order(self,**_args):
return {'menu':[]} if 'order' not in _args else _args['order']
# def menu (self,**_args):
# return {'menu':[]} if 'menu' not in _args else _args['menu']
def overwrite(self,**_args):
return {}
def header (self,**_args):
_data = {"title":"QCMS Project", "subtitle":"Powered by Python Flask"}
return self.update(_data,**_args)

@ -0,0 +1,50 @@
import json
import pandas as pd
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
# """
# _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
# return getattr(module,_name) if hasattr(module,_name) else None

File diff suppressed because one or more lines are too long

@ -0,0 +1,48 @@
"""
This class implements the base infrastructure to handle themes, the class must leverage
default themes will be stored in layout.root._assets.themes, The expected files in a theme are the following:
- borders
- buttons
- layout
- menu
- header
If the following are not available then we should load the default one.
This would avoid crashes but would come at the expense of a lack of consistent visual layout (alas)
"""
import requests
import os
URL = os.environ['QCMS_HOST_URL'] if 'QCMS_HOST_URL' in os.environ else 'https://dev.the-phi.com/qcms'
def current (_system) :
return _system['theme']
def List (_url = URL) :
"""
calling qcms to list the available URL
"""
try:
_url = '/'.join([_url,'api','themes','List'])
return requests.get(_url).json()
except Exception as e:
pass
return []
def Get(theme,_url= URL) :
"""
This function retrieves a particular theme from a remote source
The name should be in the list provided by the above function
"""
try:
_url = '/'.join([_url,'api','themes','Get']) +f'?theme={theme}'
try:
return requests.get(_url).json()
except Exception as e:
pass
return None
except Exception as e:
pass
return {}
def installed (path):
return os.listdir(os.sep.join([path,'_assets','themes']))
def Set(theme,_system) :
_system['theme'] = theme
return _system

@ -0,0 +1,274 @@
__doc__ = """
arguments :
--config path of the configuration otherwise it will look for the default in the working directory
"""
from flask import Flask,render_template,send_from_directory,request, redirect, Response, session
import flask
import transport
from transport import providers
import cms
from cms import Plugin
import sys
import os
import json
import copy
import io
import base64
from jinja2 import Environment, BaseLoader
import typer
from typing_extensions import Annotated
from typing import Optional
import pandas as pd
import uuid
import datetime
import requests
from cms import disk, cloud, engine
_app = Flask(__name__)
cli = typer.Typer()
@_app.route('/favicon.ico')
@_app.route('/<id>/favicon.ico')
def favicon(id):
global _route
# _system = _route.get ().system()
# _handler = _route.get()
_handler = _getHandler(id)
_system = _handler.system()
_logo =_system['icon'] #if 'icon' in _system else 'static/img/logo.svg'
_stream = requests.get(''.join([request.host_url,_logo]))
return "_stream",200,{"Content-Type":"image/png"} #_handler.get(_logo),200,{"content-type":"image/png"}
def _getHandler (app_id,resource=None) :
global _route
_id = _getId(app_id,resource)
return _route._apps[_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)
@_app.route("/robots.txt")
def robots_txt():
"""
This function will generate a robots expression for a variety of crawlers, the paths will be provided by
menu options
"""
global _route
_system = _route.get ().system()
_info = ['''
User-agent: *
Allow: /
''']
if 'routes' in _system :
for _key in _system['routes'] :
_uri = '/'.join(['',_key])
_info.append(f'''
User-agent: *
Allow: {_uri}
''')
# return '\n'.join(_info),200,{'Content-Type':'plain/text'}
return Response('\n'.join(_info), mimetype='text/plain')
def _getIndex (app_id ,resource=None):
_handler = _getHandler(app_id,resource)
_layout = _handler.layout()
_status_code = 200
global _route
_index_page = 'index.html'
_args = {}
try :
_uri = os.sep.join([_layout['root'],_layout['index']])
_id = _getId(app_id,resource)
_args = _route.render(_uri,'index',_id)
except Exception as e:
_status_code = 404
_index_page = '404.html'
print(e)
return render_template(_index_page,**_args),_status_code
@_app.route("/")
def _index ():
return _getIndex('main')
@_app.route("/<app>/<resource>")
@_app.route("/<app>",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/<uid>')
# def people(uid):
# """
# This function will implement hardened links that can directly "do something"
# """
# global _config
# return "0",200
@_app.route('/<app>/dialog')
@_app.route('/dialog',defaults={'app':'main'})
def _dialog (app):
# global _config
global _route
_handler = _route.get()
_uri = request.headers['uri']
_id = request.headers['dom']
# _html = ''.join(["<div style='padding:1%'>",str( e.render(**_args)),'</div>'])
_args = _route.render(_uri,'html',app) #session.get('app_id','main'))
_args['title'] = _id
return render_template('dialog.html',**_args) #title=_id,html=_html)
@_app.route("/api/<module>/<name>",defaults={'app':'main','key':None},methods=['GET','POST','DELETE','PUT'])
@_app.route("/<app>/api/<module>/<name>",defaults={'key':None},methods=['GET','POST','DELETE','PUT'])
@_app.route("/<app>/<key>/api/<module>/<name>",methods=['GET','POST','DELETE','PUT'])
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)
_delegate = cms.delegate()
return _delegate(uri=uri,handler=_handler,request=request)
@_app.route('/version')
def _version ():
global _route
_handler = _route.get()
global _config
return _handler.system()['version']
@_app.route("/reload/<key>")
def _reload(key) :
global _route
_handler = _route.get_main()
_system = _handler.system()
if not 'source' in _system :
_systemKey = None
elif 'key' in _system['source'] and _system['source']['key']:
_systemKey = _system['source']['key']
# print ([key,_systemKey,_systemKey == key])
if key and _systemKey and _systemKey == key :
_handler.reload()
return "",200
pass
return "",403
@_app.route('/reload',methods=['POST'])
def reload():
# global _route
# _handler = _route.get_main()
# _system = _handler.system()
_key = request.headers['key'] if 'key' in request.headers else None
return _reload(_key)
# if not 'source' in _system :
# _systemKey = None
# elif 'key' in _system['source'] and _system['source']['key']:
# _systemKey = _system['source']['key']
# print ([_key,_systemKey,_systemKey == _key])
# if _key and _systemKey and _systemKey == _key :
# _handler.reload()
# return "",200
# pass
# return "",403
@_app.route('/page',methods=['POST'],defaults={'app_id':'main','key':None})
@_app.route('/<app_id>/page',methods=['POST'],defaults={'key':None})
@_app.route('/<app_id>/<key>/page',methods=['POST'])
def _POST_CMSPage(app_id,key):
"""
return the content of a folder formatted for a menu
"""
# global _config
global _route
# _handler = _route.get()
# _config = _handler.config()
_handler = _getHandler(app_id,key)
_setHandler(app_id,key)
_config = _handler.config()
# _uri = os.sep.join([_config['layout']['root'],request.headers['uri']])
_uri = request.headers['uri']
if 'dom' not in request.headers :
_id = _uri.split('/')[-1].split('.')[0]
else:
_id = request.headers['dom']
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'))
return _args[_id],200
# return _html,200
@_app.route('/page',defaults={'app_id':'main','resource':None})
@_app.route('/<app_id>/page',defaults={'resource':None},methods=['GET'])
@_app.route('/<app_id>/<resource>/page',methods=['GET'])
def _cms_page (app_id,resource):
# global _config
global _route
# _handler = _route.get()
# _config = _handler.config()
_uri = request.args['uri']
# _uri = os.sep.join([_config['layout']['root'],_uri])
_title = request.args['title'] if 'title' in request.args else ''
_args = _route.render(_uri,_title,session.get(app_id,'main'))
return _args[_title],200
@cli.command()
def start (
path:Annotated[str,typer.Argument(help="path of the manifest file")]='qcms-manifest.json',
port:int=typer.Option(default=None, help="port number to serve on (will override configuration)")
) :
"""
This function is designed to start the application with its associated manifest (configuration) location
:path path to the the manifest
:shared run in shared mode i.e
"""
global _route
if os.path.exists(path) and os.path.isfile(path):
_args = {'path':path}
# if shared :
# _args['location'] = path
# _args['shared'] = shared
_args['location'] = path
_args['shared'] = True
# _route = cms.engine.Router(**_args) #path=path,shared=shared)
_route = cms.engine.basic.CMS(**_args)
# dir(_route)
# _args = _route.get().get_app()
_args = _route.get().app()
if port :
_args['port'] = port
_app.secret_key = str(uuid.uuid4())
_app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(hours=24)
_app.run(**_args)
_status = 'found'
else:
_status = 'not found'
print(f'''
manifest: {path}
status : {_status}
''')
@cli.command(name='help')
def _help() :
pass
if __name__ == '__main__' :
cli()

@ -0,0 +1,86 @@
/***
* The idea of a board is a layout with graphs where the number of digits indicates the number of items in a dashboards line items
* The value of zero suggests the size (smaller pane) and one (large panes)
* NOTE:
* obviously a board-000 and board-111 would suggest the same thing
*/
.dashboard .chart, .scalar-pane {
border-radius:8px;
padding:4px;
border:1px solid #d3d3d3;
background-image: linear-gradient(to bottom, #ffffff, #f3f3f3);
}
.dashboard .basic-board {
display:grid;
grid-template-rows: 60px calc(100% - 60px);
grid-template-columns: 100%;
gap: 10px;
padding:8px;
align-content: center;
}
.dashboard .scalar {
display:grid; grid-template-rows:auto auto; gap: 10px; align-items: center; align-content: center;}
.dashboard .scalar .value {font-size:40px; font-family: courier; font-weight: bold;}
.dashboard .scalar .label {font-size:10px; text-transform: uppercase;}
.apexcharts-title-text {
text-transform: capitalize;
}
.dashboard .board-title {grid-column:1 / span 4; text-transform:capitalize; padding:4px;}
.scalar-pane { text-align: center;
;}
.board-10{
display:grid;
grid-template-columns: calc(100% - 125px) 125px; gap: 10px;
}
.board-10 .scalar-pane { display:grid; gap: 10px;
}
.board-10 .scalar-pane .scalar {border-top:1px solid #d3d3d3}
.board-10 .scalar-pane :first-child {border-top:0px;}
/* .board-10 .scalar-pane :nth-child(3n-1){ border-top:1px solid #d3d3d3;} */
.board-00 , .board-11 {
display:grid;
grid-template-columns: repeat(2,1fr); gap:2px
}
.board-110 {
display:grid;
grid-template-columns: 2fr 2fr 150px;
gap: 10px;
}
.board-110 .scalar-pane{display:grid;}
.board-110-2 {
display:grid;
grid-template-columns: 2fr 2fr;
gap: 10px;
}
.board-110-2 .scalar-pane {grid-row:3; grid-column: 2;
display: grid;
grid-template-columns: 50% 50%; gap: 10px;}
.board-110-2 .scalar-pane > :nth-child(even){ border-left:1px solid #d3d3d3;}
.board-100 {
display:grid;
grid-template-columns: 2fr 1fr 1fr; gap: 10px;
}
.board-100-2 {
display:grid;
grid-template-columns: 2fr 1fr ; gap: 10px;
}
.board-100,.board-100-2 .scalar-pane {display:grid; gap: 10px;}
.board-100-2 > :nth-child(1){grid-row:1 / span 2}
.board-100-2 > :nth-child(2){grid-row:1; grid-column:2}
.board-100-2 .scalar-pane {grid-row:2; grid-column: 2 ; display:grid; grid-template-columns: repeat(2, 1fr); }
.board-100-2 .scalar-pane > :nth-child(even){ border-left:1px solid #d3d3d3;}

@ -0,0 +1,81 @@
/***
The basic structure of a site is as follows :
- header
- footer footer with the author ...
- menu main ways in which the site articulates itself (features)
- info area where there are miscellaneous links or text
*/
body {
font-size:16px;
font-family: sans-serif;
font-weight:lighter;
}
.bold {font-weight:bold}
.header img { width:100%; height: 100%;}
.footer {
text-align:center;
display:grid;
grid-template-columns: repeat(3,1fr);
gap:4px;
padding:8px;
font-size:12px;
color:black;
align-items: center;
align-content: center;
text-transform: capitalize;
/* background-color: rgba(255,255,255,0.8); */
grid-column: 1 /span 2;
}
.large-text {font-size: 24px; font-weight: bold;}
.active {
padding:4px;
cursor:pointer;
border:2px solid transparent ;
}
.active .fa-chevron-right { color:transparent}
.active:hover {
border-bottom-color: #4682b4;
}
.active:hover .fa-chevron-right{
color : #4682B4;
}
.highlight {
cursor:pointer;
border:4px solid transparent;
padding:4px;
}
.highlight:hover {
border-color:#4682B4 ;
}
.button-1 {
background-color:#d3d3d3;
color:#4682b4;
font-weight:bold;
cursor:pointer;
padding:15px;
}
.button-1:hover {
color:#FFFFFF;
background-color:#4682b4;
}
.dialog-title {
background-color:#FF6500;color:#FFFFFF;
text-transform:capitalize; font-weight:bold; align-items:center;display:grid; grid-template-columns:auto 32px;
}
.dialog-button {
display:grid;
grid-template-columns: auto 115px;
gap:4px;
}

@ -0,0 +1,10 @@
.dialog-title {
background-color:#FF6500;color:#FFFFFF;
text-transform:capitalize; font-weight:bold; align-items:center;display:grid; grid-template-columns:auto 32px;
}
.dialog-button {
display:grid;
grid-template-columns: auto 115px;
gap:4px;
}

@ -0,0 +1,81 @@
/**
* This file implements styling for menus i.e we have
* 1. Traditional menus and
* 2. Tabbed menus
*/
.menu {
padding:8px;
border:1px solid #CAD5E0 ;
display:grid;
grid-column: 1 / span 2;
grid-template-columns: 92px repeat(7,auto);
gap:4px;
text-transform: capitalize;
align-items: center;
}
.menu .icon {padding:4px;}
.menu .icon img {width:30px; height:30px;}
.menu .item {
font-weight:bold;
cursor:pointer;
padding:4px;
text-align: left;
}
.menu .sub-menu {
display:none;
position:absolute;
margin-top:2px;
min-width:10%;
z-index:90;
padding:8px;
font-weight:lighter;
text-align:left;
align-items:left;
background-color: rgba(255,255,255,0.8);
}
.menu .item:hover .sub-menu{
display:block;
height:auto;
}
/**
* This section shows how we use css to implement tabs i.e we will be using radio boxes and labels
*
*/
.tabs {display:grid; grid-template-columns: repeat(auto-fit,209px); gap:0px; align-content:center;
/* background-color: #f3f3f3; */
padding-top:4px;
padding-left:4px;
padding-right:4px;
}
.tabs input[type=radio] {display:none; }
.tabs input[type=radio] + label { font-weight:lighter;
border:1px solid transparent;
border-bottom-color: #CAD5E0;
background-color: #f3f3f3;
padding:8px;
padding-right:10px; padding-left:10px;
cursor:pointer
}
.tabs input[type=radio]:checked +label {
background-color: #ffffff;
border-top-right-radius: 8px;
border-top-left-radius: 8px;
font-weight:bold;
border-color: #CAD5E0;
border-bottom-color: #FFFFFF;
}

@ -0,0 +1,81 @@
/**
* components: search
*/
/* .search-box {
display:grid;
grid-template-columns: auto 64px; gap:4px;
} */
.search .frame .suggestion-frame {
width:98%;
overflow: hidden;
overflow-y: auto;
word-wrap: normal;
margin-top:2px;
border-right:1px solid #CAD5E0;
}
.search .frame .suggestion-frame .suggestions {
display:none;
position:absolute;
width:55%;
height:55%;
z-index:9;
overflow:hidden;
overflow-y: auto;
padding:4px;;
}
.search .frame .input-frame{
display:grid;
grid-template-columns: auto 32px 48px ;
gap:4px;
align-items: center;
align-content:center;
padding:4px;
background-color: rgba(255,255,255,0.8);
}
.search .frame .input-frame i {color:#000000}
.search .frame .input-frame .found { font-size:11px; color:maroon; text-align: center;}
.search .frame input[type="text"] {
padding:8px;
font-size:14px;
font-weight:normal;
border:2px solid transparent;
outline: 0px;
background-color:#D3D3D3;
}
.search .frame .suggestions .item {
display:grid;
grid-template-columns: 60px auto ;
height:60px;
gap:4px;
padding:8px;
align-items:center;
}
.search .frame .suggestions .item .title { width:85%; overflow:hidden; font-size:14px; font-weight: normal; text-transform:capitalize; text-overflow: ellipsis; white-space: nowrap;}
.search .frame .suggestions .item .author {width:80%; overflow:hidden; font-size:14px; font-weight: lighter;text-transform: capitalize;text-overflow: ellipsis; white-space: nowrap;}
.search .frame .suggestions .item .picture{
height:50px;
display:grid;
grid-template-columns: auto;
padding:4px;
}
.search .frame .suggestions .item .link {
display:grid;
grid-template-columns: auto 120px;
font-size:12px;
align-items:center;
align-content: center;
overflow:hidden;
}

@ -0,0 +1,20 @@
.source-code {
background-color: #000000; COLOR:#ffffff;
font-family: 'Courier New', Courier, monospace;
padding:8px;
padding-left:10px;
text-wrap: wrap;
width:calc(100% - 40px);
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;
color:#000000;
}
.editor .keyword {color: #4682b4; font-weight:bold}
.code-comment { font-style: italic; color:gray; font-size:13px;}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@ -0,0 +1,4 @@
<?xml version="1.0" ?><svg id="Layer_1" style="enable-background:new 0 0 512 512;" version="1.1" viewBox="0 0 512 512" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
.st0{fill:#2B79C2;}
.st1{fill:#3B4652;}
</style><g><path class="st1" d="M317.7,230c-6,0-10.9-4.9-10.9-10.9c0-6,4.9-10.9,10.9-10.9c6,0,10.9,4.9,10.9,10.9 C328.7,225.1,323.8,230,317.7,230z"/><path class="st1" d="M256,252C256,252,256,252,256,252C256,252,256,252,256,252c-6.1,0-10.9-4.9-10.9-10.9s4.9-10.9,10.9-10.9 s10.9,4.9,10.9,10.9S262,251.9,256,252z"/><path class="st1" d="M194.3,230c-6,0-10.9-4.9-10.9-10.9c0-6,4.9-10.9,10.9-10.9c6,0,10.9,4.9,10.9,10.9 C205.2,225.1,200.3,230,194.3,230z"/><path class="st0" d="M385.8,227.6c0,53.9-32.8,100.1-79.6,119.7c-3.2,1.3-6.5,2.6-9.8,3.7v-80.9h21.4c3.9,0,7-3.1,7-7V243 c10.4-3,17.9-12.6,17.9-23.9c0-13.7-11.2-24.9-24.9-24.9c-13.7,0-24.9,11.2-24.9,24.9c0,11.3,7.6,20.9,17.9,23.9v13.1h-21.4 c-3.9,0-7,3.1-7,7v90.2c0,0.5,0,0.9,0.1,1.4c-6.3,1.3-12.8,2.2-19.5,2.5v-92.3c10.3-3,17.9-12.6,17.9-23.9 c0-13.7-11.2-24.9-24.9-24.9s-24.9,11.2-24.9,24.9c0,11.3,7.6,20.9,17.9,23.9v92.3c-6.6-0.3-13.1-1.2-19.5-2.5 c0.1-0.4,0.1-0.9,0.1-1.4v-90.2c0-3.9-3.1-7-7-7h-21.4V243c10.3-3,17.9-12.6,17.9-23.9c0-13.7-11.2-24.9-24.9-24.9 c-13.8,0-24.9,11.2-24.9,24.9c0,11.3,7.6,20.9,17.9,23.9v20.1c0,3.9,3.1,7,7,7h21.4V351c-3.3-1.1-6.6-2.3-9.8-3.7 c-46.8-19.6-79.6-65.8-79.6-119.7c0-71.7,58.1-129.8,129.8-129.8S385.8,155.9,385.8,227.6z"/><path class="st1" d="M302,489.4c-4.5,6.9-12.3,11.4-21.2,11.4h-49.5c-8.9,0-16.7-4.5-21.2-11.4H302z"/><path class="st1" d="M327.4,440.3c-2.8,2.8-6.6,4.5-10.8,4.5c8.5,0,15.3,6.9,15.3,15.3c0,4.2-1.7,8-4.5,10.8 c-2.8,2.8-6.6,4.5-10.8,4.5H195.4c-8.5,0-15.3-6.9-15.3-15.3c0-4.2,1.7-8,4.5-10.8c2.8-2.8,6.6-4.5,10.8-4.5 c-8.5,0-15.3-6.9-15.3-15.3c0-4.2,1.7-8.1,4.5-10.8c2.8-2.8,6.6-4.5,10.8-4.5c-8.5,0-15.3-6.8-15.3-15.3c0-4.2,1.7-8,4.5-10.8 c2.8-2.8,6.6-4.5,10.8-4.5h10.4v-18.5c32.3,11.8,68,11.9,100.3,0.2v18.3h10.4c8.5,0,15.3,6.8,15.3,15.3c0,4.2-1.7,8.1-4.5,10.8 c-2.8,2.8-6.6,4.5-10.8,4.5c8.5,0,15.3,6.9,15.3,15.3C331.9,433.7,330.2,437.6,327.4,440.3z"/><g><path class="st1" d="M256.3,11.2c-12,0-21.8,9.8-21.8,21.8c0,9.5,6.1,17.5,14.5,20.5v13.9c0,3.9,3.1,7,7,7s7-3.1,7-7V53.6 c8.7-2.8,15-11,15-20.7C278,20.9,268.2,11.2,256.3,11.2z"/><path class="st1" d="M369.3,121.3c1.8,0,3.6-0.7,4.9-2.1l9.7-9.7c3.1,1.6,6.5,2.4,9.8,2.4c5.6,0,11.1-2.1,15.4-6.4 c4.1-4.1,6.4-9.6,6.4-15.4c0-5.8-2.3-11.3-6.4-15.4s-9.6-6.4-15.4-6.4s-11.3,2.3-15.4,6.4s-6.4,9.6-6.4,15.4 c0,3.3,0.8,6.5,2.1,9.4l-9.9,9.9c-2.7,2.7-2.7,7.2,0,9.9C365.7,120.6,367.5,121.3,369.3,121.3z"/><path class="st1" d="M450.7,206.1c-9.5,0-17.5,6.1-20.5,14.5h-13.9c-3.9,0-7,3.1-7,7s3.1,7,7,7H430c2.8,8.7,11,15,20.7,15 c12,0,21.8-9.8,21.8-21.8S462.7,206.1,450.7,206.1z"/><path class="st1" d="M393.5,343.7c-3.3,0-6.5,0.8-9.4,2.1l-9.9-9.9c-2.7-2.7-7.2-2.7-9.9,0c-2.7,2.7-2.7,7.2,0,9.9l9.7,9.7 c-1.5,3-2.4,6.4-2.4,9.8c0,5.8,2.3,11.3,6.4,15.4c0,0,0,0,0,0c4.2,4.2,9.8,6.4,15.4,6.4c5.6,0,11.1-2.1,15.4-6.4 c4.1-4.1,6.4-9.6,6.4-15.4c0-5.8-2.3-11.3-6.4-15.4C404.8,346,399.3,343.7,393.5,343.7z"/><path class="st1" d="M137.7,336l-9.7,9.7c-3-1.5-6.4-2.4-9.8-2.4c-5.8,0-11.3,2.3-15.4,6.4c0,0,0,0,0,0c-4.1,4.1-6.4,9.6-6.4,15.4 c0,5.8,2.3,11.3,6.4,15.4c4.2,4.2,9.8,6.4,15.4,6.4c5.6,0,11.1-2.1,15.4-6.4c4.1-4.1,6.4-9.6,6.4-15.4c0-3.3-0.8-6.5-2.1-9.4 l9.9-9.9c2.7-2.7,2.7-7.2,0-9.9C144.9,333.3,140.5,333.3,137.7,336z"/><path class="st1" d="M102.7,227.6c0-3.9-3.1-7-7-7H82c-2.8-8.7-11-15-20.7-15c-12,0-21.8,9.8-21.8,21.8s9.8,21.8,21.8,21.8 c9.5,0,17.5-6.1,20.5-14.5h13.9C99.6,234.6,102.7,231.5,102.7,227.6z"/><path class="st1" d="M137.9,99.6c1.5-3,2.4-6.4,2.4-9.8c0-5.8-2.3-11.3-6.4-15.4s-9.6-6.4-15.4-6.4s-11.3,2.3-15.4,6.4 s-6.4,9.6-6.4,15.4s2.3,11.3,6.4,15.4s9.6,6.4,15.4,6.4c3.3,0,6.5-0.8,9.4-2.1l9.9,9.9c1.4,1.4,3.2,2.1,4.9,2.1 c1.8,0,3.6-0.7,4.9-2.1c2.7-2.7,2.7-7.2,0-9.9L137.9,99.6z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

@ -0,0 +1,87 @@
/**
* This file has functions that allow pages to be fetched and rendered on bootup
*/
var bootup = {}
//
// We implement this function using an observer design pattern
bootup.CMSObserver = function(_context,_domId,_fileURI){
this._context = _context
this._domId = _domId
this._fileURI = _fileURI
this.apply = function (_caller){
var http = HttpClient.instance()
http.setHeader('uri',_fileURI)
uri = '/page'
if (this._context != '' && this._context != null) {
uri = this._context + uri
}
if (window.location.pathname != '/'){
uri = ([window.location.pathname,'page']).join('/')
}
try{
// var _domElement = jx.dom.get.instance('div')
// _domElement.className = 'busy-loading'
// jx.dom.append(_domId, _domElement)
http.post(uri,function(x){
if (x.status == 200 && x.readyState == 4){
setTimeout(function(){
_content = $(x.responseText)
var _id = $(_content).attr('id')
_pid = (['#',_domId,' #',_id]).join('')
if( $(_pid).length != 0){
$(_pid).remove()
}
$('#'+_domId).append(x.responseText)
},10)
}
_caller.notify()
})
}catch(error){
_caller.notify()
}
}
}
//
// Finalize the process of rendering the content on the fly
bootup.finalize = function(_id){
this.apply = function(_caller){
// menu.runScript('#'+_id)
setTimeout(function(){menu.events.finalize(_id);},1000)
// menu.events.finalize(_id)
}
}
bootup.init = function(sys_id,_layout){
if (!_layout) {
return ;
}
if (_layout.on){
jx.utils.keys(_layout.on.load).forEach(function(_domId){
var observers =
jx.utils.patterns.visitor(_layout.on.load[_domId], function(_uri){
// _uri = _layout.root_prefix != null? (_layout.root_prefix+_uri) : _uri
return new bootup.CMSObserver(sys_id,_domId,_uri)
})
observers.push(new bootup.finalize(_domId))
//
// At this point we can execute the observer design pattern
//
// console.log(observers)
jx.utils.patterns.observer(observers,'apply')
})
}
}

@ -0,0 +1,230 @@
var monitor = {render:{},context:'',get:{}}
monitor.render.table_list = function (_data){
var nodes = jx.dom.get.instance('DIV')
_data.data.forEach(_item=>{
var _div = jx.dom.get.instance('DIV')
_div.innerHTML = _item.name
_div.className = 'active'
_div._data = _item.name
//
// bind the nodes to an event
_div.onclick = function (){
monitor.get(this._data)
}
nodes.appendChild(_div)
})
//
// adding the nodes to
jx.dom.set.value('table.info',_data.about)
jx.dom.set.value('tables','')
jx.dom.append('tables',nodes)
nodes.childNodes[0].click()
}
monitor.render.data = function(_data){
jx.dom.set.value('dashboard','' )
_apex = []
Object.keys(_data).forEach(_key=>{
_board = jx.dom.get.instance('DIV')
_item = _data[_key]
_pane = jx.dom.get.instance('DIV')
// _pane.className = 'border border-round'
_item.forEach(_entry=>{
//-- _entry is the context of a set of charts ...
var _id = Object.keys(_entry)[0]
_itemPane = jx.dom.get.instance('DIV')
_frame = jx.dom.get.instance('DIV')
// _itemPane.innerHTML = '<h3>'+_id+'</h3>',_entry[_id]
//
// Making title ....
var _titleDiv = $('<div class="board-title"><div class="large-text">:title</div><div class="small">:about</div></div>'.replace(/:title/,_id).replace(/:about/,_entry[_id].about))
// $(_itemPane).append(_titleDiv)
$(_frame).append(_titleDiv)
_itemPane.className = _entry[_id].css
_entry[_id].charts.forEach(_chartItem=>{
if (_chartItem.type == 'scalar') {
_chart = jx.dom.get.instance('DIV')
_chart.className = 'scalar-pane'
// _scalar = $(_options.chart) //-- html objets converted via jquery
// console.log()
_chart.innerHTML = _chartItem.html
}else{
var _chart = jx.dom.get.instance('DIV')
_chart.className = 'chart '+_chartItem.type
_apex.push(new ApexCharts(_chart,_chartItem.options))
}
_itemPane.appendChild(_chart)
})
_frame.appendChild(_itemPane)
_frame.className = 'basic-board'
// _itemPane.className = 'board-' + _itemPane.childNodes.length
// _itemPane.className = 'board-' + _itemPane.childNodes.length
jx.dom.append('dashboard',_frame)
// var _options = _item[_id]
})
})
_apex.forEach(_chart=>{ _chart.render()})
}
monitor.get.scalar = function(_options){
return $(_options.charts)
}
monitor.render.scalar = function (_data){
var _pane = jx.dom.get.instance('DIV')
// _pane.className = 'scalar-pane'
var _nodes = []
Object.keys(_data.values).forEach(_key=>{
var _div = jx.dom.get.instance('DIV')
var _value = jx.dom.get.instance('DIV')
_value.innerHTML = _data.values[_key]
var _label = jx.dom.get.instance('DIV')
_label.innerHTML = _key
// _div.className = 'scalar border-round border'
_value.className = 'value'
_label.className = 'label'
_div.appendChild(_value)
_div.appendChild(_label)
// _pane.appendChild(_div)
_nodes.push(_div)
})
return _nodes
}
monitor.render.grid = function (_data){
var pane = jx.dom.get.instance('TABLE')
var header=jx.dom.get.instance('TR')
pane.appendChild(header)
columns = {}
_data.forEach(_row=>{
var rowItem = jx.dom.get.instance('TR')
Object.keys(_row).forEach(_col =>{
if(columns[_col] == null){
columns[_col] = jx.dom.get.instance('TD')
// columns[_col].className = 'bold border-bottom'
columns[_col].innerHTML = _col
}
var _value = _row[_col]
_div = jx.dom.get.instance('TD')
_div.innerHTML = _value
rowItem.appendChild(_div)
})
pane.appendChild(rowItem)
})
//
//
Object.keys(columns).forEach(_key=>{
header.appendChild(columns[_key])
})
pane.className = 'data-grid border-round border'
return pane
}
monitor.init = function (_context){
var _uri = '/api/logger/init'
if (_context ){
monitor.context = _context
_uri = _context + _uri
}
var http = HttpClient.instance()
http.post(_uri,function (x){
if (x.readyState == 4 && x.status == 200){
tables = JSON.parse(x.responseText)
monitor.render.table_list (tables)
}else{
//
// gracefully handle this ....
}
})
}
monitor.get = function(table){
var uri = monitor.context+'/api/logger/read'
var http = HttpClient.instance()
http.setData(JSON.stringify({'name':table}))
http.setHeader('Content-Type','application/json')
http.post(uri,function(x){
if(x.status == 200 && x.readyState == 4){
var _data = JSON.parse(x.responseText)
monitor.render.data(_data)
}
})
}
if(!qcms){
var qcms = {}
}
qcms.dashboard = function(_uri,_id){
this._uri = _uri
this._id = _id
this.get = function (){
var _uri = this._uri ;
var http = HttpClient.instance()
http.setHeader('Content-Type','application/json')
//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){
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 ;
}
}
}

@ -0,0 +1,44 @@
var dialog = {}
dialog.context = ''
dialog.show = function(_args,_pointer){
var http = HttpClient.instance()
// 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()
jx.modal.show({html:x.responseText,id:'body'})
if(jx.dom.exists('dialog-message') && _args.message != null){
jx.dom.set.value('dialog-message',_args.message)
}
//
// In order to perhaps execute any js script that should have been executed on load ...
//
var scripts = $('.jxmodal script')
jx.utils.patterns.visitor(scripts,function(_item){
if(_item.text.trim().length > 0){
var _routine = eval(_item.text)
//
//@TODO:
// Find a way to add the running function into the page to enable scripts to work
//
}
})
if (_pointer !=null){
_pointer()
}
})
}
if (! qcms){
var qcms = {}
}
qcms.dialog = dialog

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save