@ -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()
|
@ -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(""",'"').replace("<","<").replace(">",">") 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(""",'"').replace("<","<").replace(">",">") 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
|
||||
|
||||
|
@ -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;}
|
After Width: | Height: | Size: 1.9 MiB |
After Width: | Height: | Size: 2.5 MiB |
After Width: | Height: | Size: 3.3 MiB |
After Width: | Height: | Size: 1.6 MiB |
After Width: | Height: | Size: 786 KiB |
After Width: | Height: | Size: 1.7 MiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 116 KiB |
After Width: | Height: | Size: 396 KiB |
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
|