@ -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
|