Merge pull request 'v2.1 - menu, themes, runner' (#10) from v2.1 into master

Reviewed-on: cloud/cms#10
master
Steve L. Nyemba 3 months ago
commit bde8d235b3

@ -5,30 +5,33 @@ import sys
import json
import importlib
import importlib.util
from git.repo.base import Repo
# 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
start = index.start
from cms.engine.config.structure import Layout, System
from cms.engine import project, config, themes, plugins
import pandas as pd
import requests
__doc__ = """
(c) 2022 Quick Content Management System - QCMS
Health Information Privacy Lab - Vanderbilt University Medical Center
MIT License
Is a Python/Flask template for creating websites using disk structure to build the site and loading custom code
The basic template for a flask application will be created in a specified location, within this location will be found a folder "content"
- within the folder specify the folder structure that constitute the menu
Usage :
qcms --create <path> --version <value> --title <title> --subtitle <subtitle>
"""
start = index.start
__doc__ = f"""
Built and designed by Steve L. Nyemba, steve@the-phi.com
version {meta.__version__}
{meta.__license__}"""
PASSED = ' '.join(['[',colored(u'\u2713', 'green'),']'])
FAILED= ' '.join(['[',colored(u'\u2717','red'),']'])
@ -40,94 +43,6 @@ INVALID_FOLDER = """
# handling cli interface
cli = typer.Typer()
def _get_config (path) :
if os.path.exists(path) :
f = open(path)
_conf = json.loads(f.read())
f.close()
else:
_conf = {}
return _conf
def _isvalid(_allowed,**_args):
if not list(set(_allowed) - set(_args.keys())) :
_pargs = {}
for key in _allowed :
_pargs [key] = _args[key]
return _pargs
return False
def write_config(_config, path):
f = open(path,'w')
f.write( json.dumps(_config)) ;
f.close()
def _system(**_args):
"""
Both version and context must be provided otherwise they are ignored
:version
:context context
"""
_info = _isvalid(['version','context'],**_args)
_info['theme'] = 'default.css'
_info = _info if _info else {'version':'0.0.0','context':'','theme':'default.css'}
if _info :
_info['logo'] = None if 'logo' not in _args else _args['logo']
#
# There is an aggregation entry here in app
_appInfo = {'debug':True,'port':8084,'threaded':True,'host':'0.0.0.0'}
if 'app' not in _args:
_info['app'] = _appInfo
else:
_info['app'] = dict(_appInfo,**_args['app'])
return _info
def _header(**_args):
return _isvalid(['logo','title','subtitle'],**_args)
def _layout(**_args):
_info = _isvalid(['root','index'],**_args)
_info['on'] = {"load":{},"error":{}}
_url = 'qcms.co'
_overwrite = {"folder1":{"type":"redirect","url":_url},"folder2":{"type":"dialog"},"folder3":{"type":"dialog","url":_url}}
_info['icons'] = {"comment":"use folder names as keys and fontawesome type as values to add icons to menu"}
_info["api"] = {"comment":"use keys as uri and function calls as values"}
_info['map'] = {},
_info['order'] = {'menu':[]}
_info['overwrite'] = _overwrite
return _info
def make (**_args):
"""
:context
:port port to be served
:title title of the application
:root folder of the content to be scanned
"""
if 'port' in _args :
_args['app'] = {'port':_args['port']}
del _args['port']
if 'context' not in _args :
_args['context'] = ''
_info ={'system': _system(**_args)}
_hargs = {'title':'QCMS'} if 'title' not in _args else {'title':_args['title']}
_hargs['subtitle'] = '' if 'subtitle' not in _args else _args['subtitle']
_hargs['logo'] = True
_info['layout'] = _layout(root=_args['root'],index='index.html')
_info['layout']['header'] = _header(**_hargs)
#
if 'footer' in _args :
_info['layout']['footer'] = [{'text':_args['footer']}] if type(_args['footer']) != list else [{'text':term} for term in _args['footeer']]
else:
_info['layout']['footer'] = [{'text':'Vanderbilt University Medical Center'}]
return _info
@cli.command(name="info")
def _info():
@ -135,23 +50,23 @@ def _info():
This function returns metadata information about this program, no parameters are needed
"""
print ()
print (meta.__name__,meta.__version__)
print ('QCMS',meta.__version__)
print (meta.__author__,meta.__email__)
print ()
@cli.command(name='set-app')
@cli.command(name='setup')
# def set_app (host:str="0.0.0.0",context:str="",port:int=8084,debug=True):
def set_app (host:Annotated[str,typer.Argument(help="bind host IP address")]="0.0.0.0",
context:Annotated[str,typer.Argument(help="if behind a proxy server (no forward slash needed)")]="",
port:Annotated[int,typer.Argument(help="port on which to run the application")]=8084,
debug:Annotated[bool,typer.Argument(help="set debug mode on|off")]=True):
"""
This function consists in editing application access i.e port, debug, and/or context
Setup application access i.e port, debug, and/or context
These parameters are the same used in any flask application
:context alias or path for applications living behind proxy server
"""
global INVALID_FOLDER
_config = _get_config()
_config = config.get()
if _config :
_app = _config['system']['app']
_app['host'] = host
@ -159,21 +74,18 @@ def set_app (host:Annotated[str,typer.Argument(help="bind host IP address")]="0.
_app['debug'] = debug
_config['system']['context'] = context
_config['app'] = _app
write_config(_config)
config.write(_config)
_msg = f"""{PASSED} Successful update, good job !
"""
else:
_msg = INVALID_FOLDER
print (_msg)
@cli.command(name='set-cloud')
@cli.command(name='cloud')
# def set_cloud(path:str): #url:str,uid:str,token:str):
def set_cloud(path:Annotated[str,typer.Argument(help="path of the auth-file for the cloud")]): #url:str,uid:str,token:str):
"""
This function overwrites or sets token information for nextcloud. This function will load the content from the cloud instead of locally
:url url of the nextcloud
:uid account identifier
:token token to be used to signin
Setup qcms to generate a site from files on nextcloud
The path must refrence auth-file (data-transport)
"""
if os.path.exists(path):
f = open (path)
@ -182,13 +94,12 @@ def set_cloud(path:Annotated[str,typer.Argument(help="path of the auth-file for
url = _auth['url']
if os.path.exists('qcms-manifest.json') :
_config = _get_config()
_config = config.get()
_config['system']['source'] = path #{'id':'cloud','auth':{'url':url,'uid':uid,'token':token}}
write_config(_config)
config.write(_config)
title = _config['layout']['header']['title']
_msg = f"""
Successfully update, good job!
{url}
_msg = f"""{PASSED} Successfully update, good job!
{url}
"""
else:
_msg = INVALID_FOLDER
@ -197,13 +108,15 @@ def set_cloud(path:Annotated[str,typer.Argument(help="path of the auth-file for
else:
_msg = INVALID_FOLDER
print (_msg)
@cli.command(name='set-key')
@cli.command(name='secure')
# def set_key(path):
def set_key(
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 to control the application during developement. The location must have been created prior.
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
"""
if not os.path.exists(keyfile):
@ -211,11 +124,11 @@ def set_key(
f.write(str(uuid.uuid4()))
f.close()
#
_config = _get_config(manifest)
_config = config.get(manifest)
if 'source' not in _config['system']:
_config['system']['source'] = {'id':'disk'}
_config['system']['source']['key'] = keyfile
write_config(_config,manifest)
config.write(_config,manifest)
_msg = f"""{PASSED} A key was generated and written to {keyfile}
use this key in header to enable reload of the site ...`
"""
@ -252,103 +165,191 @@ def load(**_args):
return getattr(module,_name) if hasattr(module,_name) else None
@cli.command(name='plugins')
def plug_info (manifest:Annotated[str,typer.Argument(help="path to manifest file")],
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,
"""
_config = config.get(manifest)
_root = os.sep.join(manifest.split(os.sep)[:-1] + [_config['layout']['root'],'_plugins'])
if os.path.exists(_root) :
# files = os.listdir(_root)
_msg = f"""{FAILED} no operation was specified, please use --help option"""
# if 'plugins' in _config :
_plugins = _config['plugins'] if 'plugins' in _config else {}
if show :
if not _plugins :
_msg = f"""{FAILED} no plugins are loaded\n\t{manifest}"""
else:
_data = plugins.stats(_plugins)
print (_data)
_msg = f"""{PASSED} found a total of {_data.loaded.sum()} plugins loaded from {_data.shape[0]} file(s)\n\t{_root}"""
if add in [True,False] and pointer :
file,fnName = pointer.split('.')
_fnpointer = plugins.load(_root,file+'.py',fnName)
if _fnpointer and add:
if file not in _plugins :
_plugins[file] = []
if fnName not in _plugins[file] :
_plugins[file].append(fnName)
_msg = f"""{PASSED} registered {pointer}, use the --show option to list loaded plugins\n{manifest} """
else:
_msg = f"""{FAILED} could not register {pointer}, it already exists\n\t{manifest} """
elif add is False and file in _plugins:
_plugins[file] = [_name.strip() for _name in _plugins[file] if _name.strip() != fnName.strip() ]
_msg = f"""{PASSED} unregistered {pointer}, use the --show option to list loaded plugins\n{manifest} """
#
# We need to write this down !!
if add in [True,False] :
_config['plugins'] = _plugins
config.write(_config,manifest)
# else:
# _msg = f"""{FAILED} no plugins are loaded\n\t{manifest}"""
print()
print(_msg)
else:
_msg = f"""{FAILED} no plugin folder found """
pass
@cli.command (name='create')
# def create(folder:str,root:str,index:str='index.html',title:str='qcms',subtitle:str='',footer:str='Quick Content Management System',version:str='0.1'):
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)")],
# 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="Quick Content Management System",help="text to be placed as footer"), #Annotated[str,typer.Argument(help="text on the footer of the main page")]='Quick Content Management System',
version:str=typer.Option(default="0.2",help="version number") #Annotated[str,typer.Argument(help="version of the site")]='0.1'
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'
):
"""
This function will create a project folder by performing a git clone (pull the template project)
Create a project folder by performing a git clone (pull the template project)
and adding a configuration file to that will bootup the project
folder project folder
root name/location of root folder associated with the web application
@TODO:
- add port,
"""
#global SYS_ARGS
#folder = SYS_ARGS['create']
url = "https://dev.the-phi.com/git/cloud/cms.git"
#if 'url' in SYS_ARGS :
# url = SYS_ARGS['url']
print ("\tcreating project into folder " +folder)
#
# if standalone, then we should create the git repository
# if standalone :
# _repo = Repo.clone_from(url,folder)
# #
# # removing remote folder associated with the project
# _repo.delete_remote(remote='origin')
# else:
# os.makedirs(folder)
#
# @TODO: root should be handled in
rootpath = os.sep.join([folder,root])
_img = None
if not os.path.exists(rootpath) :
print (f"{PASSED} Creating content folder "+rootpath)
os.makedirs(rootpath)
os.makedirs(os.sep.join([rootpath,'_images']))
_img = """data:image/png;base64,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"""
_,_img = _img.split(',',1)
logo = os.sep.join([rootpath,'_images','logo.png'])
f = open(logo,'wb')
f.write(base64.b64decode(_img))
f.close()
_html = __doc__.replace("<","&lt;").replace(">","&gt;").replace("Usage","<br>Usage")
if not os.path.exists(os.sep.join([rootpath,'index.html'])) :
f = open(os.sep.join([rootpath,'index.html']),'w')
f.write(_html)
f.close()
print (f'{PASSED} configuration being written to project folder')
_args = {'title':title,'subtitle':subtitle,'version':version,'root':root,'index':index}
if footer :
_args['footer'] = footer
_config = make(**_args)
#
# updating logo reference (default for now)
_config['system']['logo'] = os.sep.join([_config['layout']['root'],'_images/logo.png'])
# 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
#
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
""")
f = open(os.sep.join([folder,'qcms-manifest.json']),'w')
f.write(json.dumps(_config))
f.close()
return _config
@cli.command (name='reload')
def reload (path) :
if os.path.exists (path):
pass
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 ...
"""
_config = config.get(path)
if 'key' in _config['system']['source'] :
f = open(_config['system']['source']['key'])
key = f.read()
f.close()
_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} 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"""
print (_msg)
@cli.command(name="bootup")
def bootup (
path:Annotated[str,typer.Argument(help="path of the manifest file")]='qcms-manifest.json',
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
"""
index.start(path,port)
# if not port :
# index.start(path)
# else:
# index.start(path,port)
def reset():
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'])
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
"""
_config = config.get(manifest)
_root = os.sep.join( manifest.split(os.sep)[:-1]+[_config['layout']['root']])
if show :
_df = pd.DataFrame({"available":themes.List()})
if _df.shape[0] > 0 :
values = themes.installed(_root)
values.sort()
values = values + np.repeat(f"""{FAILED}""", _df.shape[0] - len(values)).tolist()
values.sort()
_df['installed'] = values
else:
_df = f"""{FAILED} No themes were found in registry,\ncurl {themes.URL}/api/themes/List (QCMS_HOST_URL should be set)"""
print (_df)
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
config.write(_config,manifest)
_msg = f"""{PASSED} successfully downloaded {name} \n{PASSED} updated manifest {manifest}
"""
except Exception as e:
_msg = f"""{FAILED} operation failed "{str(e)}"
"""
pass
print (_msg)
global SYS_ARGS
if __name__ == '__main__':
cli()

@ -6,192 +6,62 @@ import copy
from jinja2 import Environment, BaseLoader, FileSystemLoader
import importlib
import importlib.util
# from cms import disk, cloud, engine
# import cloud
# import index
# class components :
# # @staticmethod
# # def folders (_path):
# # """
# # 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('_')]
# # @staticmethod
# # def content (_folder) :
# # if os.path.exists(_folder) :
# # # return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_folder,_name])} for _name in os.listdir(_folder) if not _name.startswith('_') and os.path.isfile( os.sep.join([_folder,_name]))]
# # return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_folder,_name])} for _name in os.listdir(_folder) if not _name.startswith('_') and os.path.isfile( os.sep.join([_folder,_name]))]
# # else:
# # return []
# @staticmethod
# def menu(_config):
# """
# This function will read menu and sub-menu items from disk structure,
# The files are loaded will
# """
# # _items = components.folders(_path)
# # _layout = copy.deepcopy(_config['layout'])
# # _overwrite = _layout['overwrite'] if 'overwrite' in _layout else {}
# #
# # content of each menu item
# # _subItems = [ components.content (os.sep.join([_path,_name]))for _name in _items ]
# # if 'map' in _layout :
# # _items = [_name if _name not in _layout['map'] else _layout['map'][_name] for _name in _items]
# # _object = dict(zip(_items,_subItems))
# if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' :
# _sourceHandler = cloud
# else:
# _sourceHandler = disk
# _object = _sourceHandler.build(_config)
# # _object = disk.build(_path,_config) if type(_path) == str else cloud.build(_path,_config)
# _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:
# _item['uri'] = _item['uri'].replace(_layout['root'],'')
# _submenu[_index] = _item
# _index += 1
# return _object
# @staticmethod
# def html(uri,id,_args={},_system={}) :
# """
# This function reads a given uri and returns the appropriate html document, and applies environment context
# """
# if 'source' in _system and _system['source']['id'] == 'cloud':
# _html = cloud.html(uri,dict(_args,**{'system':_system}))
# else:
# _html = disk.html(uri)
# # _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)
# #
# # If the rendering of the HTML happens here we should plugin custom functions (at the very least)
# #
# return appContext.render(**_args)
# # return _html
# @staticmethod
# def data (_args):
# """
# :store data-store parameters (data-transport, github.com/lnyemba/data-transport)
# :query query to be applied against the store (expected data-frame)
# """
# _store = _args['store']
# reader = transport.factory.instance(**_store)
# _queries= copy.deepcopy(_store['query'])
# _data = reader.read(**_queries)
# return _data
# @staticmethod
# def csv(uri) :
# return pd.read(uri).to_html()
# @staticmethod
# def load_plugin(**_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
# @staticmethod
# def plugins(_config) :
# """
# This function looks for plugins in the folder on disk (no cloud support) and attempts to load them
# """
# PATH= os.sep.join([_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 = components.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)
# return _map
# @staticmethod
# def context(_config):
# """
# adding custom variables functions to Jinja2, this function should be called after plugins are loaded
# """
# _plugins = _config['plugins']
# # if not location:
# # env = Environment(loader=BaseLoader())
# # else:
# location = _config['layout']['root']
# # env = Environment(loader=FileSystemLoader(location))
# env = Environment(loader=BaseLoader())
# # env.globals['routes'] = _config['plugins']
# return env
# @staticmethod
# def get_system(_config,skip_keys=[]):
# _system = copy.deepcopy(_config['system'])
# if skip_keys :
# for key in skip_keys :
# if key in _system :
# del _system
# return _system
# def _get_config (path) :
# if os.path.exists(path) :
# f = open(path)
# _conf = json.loads(f.read())
# f.close()
# else:
# _conf = {}
# return _conf
# def _isvalid(_allowed,**_args):
# if not list(set(_allowed) - set(_args.keys())) :
# _pargs = {}
# for key in _allowed :
# _pargs [key] = _args[key]
# return _pargs
# return False
# def write_config(_config, path):
# f = open(path,'w')
# f.write( json.dumps(_config)) ;
# f.close()
# def _system(**_args):
# """
# Both version and context must be provided otherwise they are ignored
# :version
# :context context
# """
# _info = _isvalid(['version','context'],**_args)
# _info['theme'] = 'default.css'
# _info = _info if _info else {'version':'0.0.0','context':'','theme':'default.css'}
# if _info :
# _info['logo'] = None if 'logo' not in _args else _args['logo']
# #
# # There is an aggregation entry here in app
# _appInfo = {'debug':True,'port':8084,'threaded':True,'host':'0.0.0.0'}
# if 'app' not in _args:
# _info['app'] = _appInfo
# else:
# _info['app'] = dict(_appInfo,**_args['app'])
# return _info
# def _header(**_args):
# return _isvalid(['logo','title','subtitle'],**_args)
# def _layout(**_args):
# _info = _isvalid(['root','index'],**_args)
# _info['on'] = {"load":{},"error":{}}
# _url = 'qcms.co'
# _overwrite = {"folder1":{"type":"redirect","url":_url},"folder2":{"type":"dialog"},"folder3":{"type":"dialog","url":_url}}
# _info['icons'] = {"comment":"use folder names as keys and fontawesome type as values to add icons to menu"}
# _info["api"] = {"comment":"use keys as uri and function calls as values"}
# _info['map'] = {},
# _info['order'] = {'menu':[]}
# _info['overwrite'] = _overwrite
# return _info

@ -128,13 +128,15 @@ def html (uri,_config) :
_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 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))
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) if uri[-2:] in ['md','MD','Md','mD'] else _html
return markdown(_html).replace("&quot;",'"').replace("&lt;","<").replace("&gt;",">") if uri[-2:] in ['md','MD','Md','mD'] else _html
# def update (_config):
# """
# This function updates the configuration provided by loading default plugins

@ -5,8 +5,9 @@ import os
import importlib
import importlib.util
import copy
import mistune
from mistune import markdown
import re
def folders (_path,_config):
"""
@ -47,6 +48,7 @@ def build (_config, keep=[]): #(_path,_content):
_path = _realpath(_path,_config)
# print (_path)
_items = folders(_path,_config)
_subItems = [ content (os.sep.join([_path,_name]),_config)for _name in _items ]
_r = {}
@ -59,6 +61,7 @@ def build (_config, keep=[]): #(_path,_content):
_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)
@ -86,13 +89,22 @@ def read (**_args):
_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()
return _stream
return None
#
# Inferring the type of the data to be returned
_mimeType = 'application/octet-stream'
_extension = _uri.split('.')[-1]
if _extension in ['css','js','csv'] :
_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'])
@ -103,7 +115,9 @@ def exists(**_args):
def html(_uri,_config) :
# _html = (open(uri)).read()
_path = _realpath(_uri,_config)
_context = _config['system']['context']
_context = str(_config['system']['context'])
# if '/' in _context :
# _context = _context.split('/')[-1]
_html = ( open(_path)).read()
_layout = _config['layout']
if 'location' in _layout :
@ -111,9 +125,11 @@ def html(_uri,_config) :
_api = os.sep.join(['api/disk/read?uri=',_layout['root']])
else:
_api = os.sep.join([f'{_context}/api/disk/read?uri=',_layout['root']])
if f"{_layout['root']}{os.sep}" in _html :
_html = _html.replace(f"{_layout['root']}",_api)
_html = mistune.html(_html).replace("&quot;",'"').replace("&lt;","<").replace("&gt;",">") if _uri[-2:] in ['md','MD','Md','mD'] else _html
_html = _html.replace(_layout['root'],_api)
_html = markdown(_html) if _uri[-2:] in ['md','MD','Md','mD'] else _html
return _html
def plugins (**_args):
"""

@ -1,384 +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
# 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']
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']
# 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']

@ -20,7 +20,7 @@ class Initializer :
"""
def __init__(self,**_args):
self._config = {'system':{},'layout':{},'plugins':{}}
self._shared = False if not 'shared' in _args else _args['shared']
# 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 {}
@ -29,8 +29,13 @@ class Initializer :
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):
@ -39,7 +44,7 @@ class Initializer :
self._isource()
self._imenu()
self._iplugins()
self._iroutes ()
# self._ISCLOUD = 'source' in self._config['system'] and self._config['system']['source']['id'] == 'cloud'
@ -52,7 +57,15 @@ class Initializer :
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) :
@ -252,8 +265,9 @@ class Initializer :
_callerContext = self._caller.system()['context']
if not self._config['system']['context'] :
self._config['system']['context'] = _callerContext
self._config['system']['caller'] = {'icon': 'caller/main/'+self._caller.system()['icon'].replace(_callerContext,'')}
_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:
@ -278,14 +292,15 @@ class Initializer :
else:
_icon = f'{_context}/api/disk/read?uri={_logo}'
if disk.exists(uri=_logo,config=self._config):
_icon = _logo
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):
@ -312,6 +327,15 @@ class Module (Initializer):
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']
@ -361,6 +385,7 @@ class Module (Initializer):
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)
@ -402,7 +427,6 @@ class CMS:
_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

@ -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,42 @@
import json
import pandas as pd
import importlib
import importlib.util
import os
def stats (_config) :
"""
Returns the statistics of the plugins
"""
_data = []
for _name in _config :
_log = {"files":_name,"loaded":len(_config[_name]),"logs":json.dumps(_config[_name])}
_data.append(_log)
return pd.DataFrame(_data)
pass
def load(_path,_filename,_name) :
"""
This function will load external module form a given location and return a pointer to a function in a given module
:path absolute path of the file (considered plugin) to be loaded
:name name of the function to be applied
"""
# _path = _args['path'] #os.sep.join([_args['root'],'plugin'])
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)
return getattr(module,_name) if hasattr(module,_name) else None

File diff suppressed because one or more lines are too long

@ -0,0 +1,44 @@
"""
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_HOME_URL'] if 'QCMS_HOME_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}'
return requests.get(_url).json()
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

@ -22,35 +22,34 @@ 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')
# def favicon():
# global _route
# _system = _route.get ().system()
# _handler = _route.get()
# _logo =_system['icon'] if 'icon' in _system else 'static/img/logo.svg'
# return _handler.get(_logo)
# # # _root = _route.get().config()['layout']['root']
# # # print ([_system])
# # # if 'source' in _system and 'id' in _system['source'] and (_system['source']['id'] == 'cloud'):
# # # uri = f'/api/cloud/downloads?doc=/{_logo}'
# # # print (['****' , uri])
# # # return redirect(uri,200) #,{'content-type':'application/image'}
# # # else:
# # # return send_from_directory(_root, #_app.root_path, 'static/img'),
# # _logo, mimetype='image/vnd.microsoft.icon')
def _getHandler () :
_id = session.get('app_id','main')
@_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 _setHandler (id) :
session['app_id'] = id
def _getId(app_id,resource):
return '/'.join([app_id,resource]) if resource else app_id
def _setHandler (app_id,resource) :
session['app_id'] = _getId(app_id,resource)
@_app.route("/robots.txt")
def robots_txt():
"""
@ -75,43 +74,55 @@ def robots_txt():
# 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 ():
# global _config
# global _route
# _handler = _route.get()
# _config = _route.config()
_handler = _getHandler()
_config = _handler.config()
print ([' serving ',session.get('app_id','NA'),_handler.layout()['root']])
# _system = _handler.system()
# _plugins= _handler.plugins()
# _args = {}
# # if 'plugins' in _config :
# # _args['routes']=_config['plugins']
# # _system = cms.components.get_system(_config) #copy.deepcopy(_config['system'])
# _html = ""
_args={'system':_handler.system(skip=['source','app','data']),'layout':_handler.layout()}
try:
uri = os.sep.join([_config['layout']['root'], _config['layout']['index']])
# # _html = _route.get().html(uri,'index',_config,_system)
# _html = _handler.html(uri,'index')
_index_page = "index.html"
_args = _route.render(uri,'index',session.get('app_id','main'))
except Exception as e:
# print ()
print (e)
_index_page = "404.html"
# _args['uri'] = request.base_url
# pass
# # if 'source' in _system :
# # del _system['source']
# _args = {'layout':_config['layout'],'index':_html}
# _args['system'] = _handler.system(skip=['source','app','route'])
return render_template(_index_page,**_args),200 if _index_page != "404.html" else 200
return _getIndex('main')
# def _xindex ():
# _handler = _getHandler()
# _config = _handler.config()
# global _route
# # print ([' serving ',session.get('app_id','NA'),_handler.layout()['root']])
# _args={'system':_handler.system(skip=['source','app','data']),'layout':_handler.layout()}
# try:
# uri = os.sep.join([_config['layout']['root'], _config['layout']['index']])
# _index_page = "index.html"
# _args = _route.render(uri,'index',session.get('app_id','main'))
# # _setHandler('main')
# except Exception as e:
# # print ()
# print (e)
# _index_page = "404.html"
# return render_template(_index_page,**_args),200 if _index_page != "404.html" else 200
@_app.route("/<app>/<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):
# """
@ -132,23 +143,28 @@ def _dialog ():
_args = _route.render(_uri,'html',session.get('app_id','main'))
_args['title'] = _id
return render_template('dialog.html',**_args) #title=_id,html=_html)
@_app.route("/caller/<app>/api/<module>/<name>")
def _delegate_call(app,module,name):
global _route
_handler = _route._apps[app]
@_app.route("/api/<module>/<name>",defaults={'app':'main','key':None})
@_app.route("/<app>/api/<module>/<name>",defaults={'key':None})
@_app.route("/<app>/<key>/<module>/<name>",defaults={'key':None})
def _delegate_call(app,key,module,name):
_handler = _getHandler(app,key)
return _delegate(_handler,module,name)
@_app.route('/api/<module>/<name>')
def _getproxy(module,name) :
# @_app.route('/api/<module>/<name>')
@_app.route("/<app_id>/<key>/api/<module>/<name>", methods=['GET'])
@_app.route("/api/<module>/<name>",defaults={'app_id':'main','key':None})
@_app.route("/<app_id>/api/<module>/<name>",defaults={'key':None})
def _api(app_id,key,module,name) :
"""
This endpoint will load a module and make a function call
:_module entry specified in plugins of the configuration
:_name name of the function to execute
"""
# global _config
# global _route
# _handler = _route.get()
_handler = _getHandler()
_handler = _getHandler( app_id,key)
return _delegate(_handler,module,name)
def _delegate(_handler,module,name):
@ -160,7 +176,7 @@ def _delegate(_handler,module,name):
_context = _handler.system()['context']
if _context :
uri = f'{_context}/{uri}'
_mimeType = 'application/octet-stream'
if uri not in _plugins :
_data = {}
_code = 404
@ -171,19 +187,30 @@ def _delegate(_handler,module,name):
# else:
# _data = pointer()
_data = pointer(request=request,config=_handler.config())
_data,_mimeType = pointer(request=request,config=_handler.config())
_mimeType = 'application/octet-stream' if not _mimeType else _mimeType
if type(_data) == pd.DataFrame :
_data = _data.to_dict(orient='records')
if type(_data) == list:
_data = json.dumps(_data)
_code = 200 if _data else 500
return _data,_code
@_app.route("/api/<module>/<name>" , methods=['POST'])
def _post (module,name):
return _data,_code,{'Content-Type':_mimeType}
# @_app.route("/api/<module>/<name>" , methods=['POST'],defaults={'app_id':'main','key':None})
# @_app.route('/<app_id>/api/<module>/<name>',methods=['POST'],defaults={'key':None})
# @_app.route('/<app_id>/<key>/api/<module>/<name>',methods=['POST'],defaults={'app_id':'main','key':None})
@_app.route("/<app_id>/<key>/api/<module>/<name>", methods=['POST'])
@_app.route("/api/<module>/<name>",defaults={'app_id':'main','key':None},methods=['POST'])
@_app.route("/<app_id>/api/<module>/<name>",defaults={'key':None},methods=['POST'])
def _post (app_id,key,module,name):
# global _config
# global _route
# _handler = _route.get()
_handler = _getHandler()
# app_id = '/'.join([app_id,key]) if key else app_id
_handler = _getHandler(app_id,key)
return _delegate(_handler,module,name)
@_app.route('/version')
@ -192,25 +219,46 @@ def _version ():
_handler = _route.get()
global _config
return _handler.system()['version']
@_app.route('/reload',methods=['POST'])
def reload():
@_app.route("/reload/<key>")
def _reload(key) :
global _route
_handler = _route.get_main()
_system = _handler.system()
_key = request.headers['key'] if 'key' in request.headers else None
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 :
print ([key,_systemKey,_systemKey == key])
if key and _systemKey and _systemKey == key :
_handler.reload()
return "",200
pass
return "",403
@_app.route('/page',methods=['POST'])
def cms_page():
@_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
"""
@ -218,7 +266,9 @@ def cms_page():
global _route
# _handler = _route.get()
# _config = _handler.config()
_handler = _getHandler()
_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']
@ -241,11 +291,13 @@ def cms_page():
if 'read?uri=' in _uri or 'download?doc=' in _uri :
_uri = _uri.split('=')[1]
_args = _route.render(_uri,_id,session.get('app_id','main'))
_args = _route.render(_uri,_id,_getId(app_id,key)) #session.get(app_id,'main'))
return _args[_id],200
# return _html,200
@_app.route('/page')
def _cms_page ():
@_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()
@ -254,33 +306,33 @@ def _cms_page ():
# _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'))
_args = _route.render(_uri,_title,session.get(app_id,'main'))
return _args[_title],200
@_app.route('/set/<id>')
def set(id):
global _route
_setHandler(id)
# _route.set(id)
# _handler = _route.get()
_handler = _getHandler()
_context = _handler.system()['context']
_uri = f'/{_context}'.replace('//','/')
return redirect(_uri)
@_app.route('/<id>')
def _open(id):
global _route
# _handler = _route.get()
# @_app.route('/set/<id>')
# def set(id):
# global _route
# _setHandler(id)
# # _route.set(id)
# # _handler = _route.get()
# _handler = _getHandler()
# _context = _handler.system()['context']
# _uri = f'/{_context}'.replace('//','/')
# return redirect(_uri)
# @_app.route('/<id>')
# def _open(id):
# global _route
# # _handler = _route.get()
_handler = _getHandler()
if id not in _route._apps :
# _handler = _getHandler()
# if id not in _route._apps :
_args = {'config':_handler.config(), 'layout':_handler.layout(),'system':_handler.system(skip=['source','app'])}
return render_template("404.html",**_args)
else:
_setHandler(id)
# _route.set(id)
return _index()
# _args = {'config':_handler.config(), 'layout':_handler.layout(),'system':_handler.system(skip=['source','app'])}
# return render_template("404.html",**_args)
# else:
# _setHandler(id)
# # _route.set(id)
# return _index()
@cli.command()
@ -307,6 +359,7 @@ def start (
# _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()

@ -1,16 +0,0 @@
"""
These are a few default plugins that will be exported and made available to the calling code
The purpose of plugins is to perform data processing
"""
import json
def copyright (_args) :
return {"author":"Steve L. Nyemba","email":"steve@the-phi.com","organization":"The Phi Technology","license":"MIT", "site":"https://dev.the-phi.com/git/cloud/qcms"}
def log (_args):
"""
perform logging against duckdb
"""
pass

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

@ -1,3 +1,8 @@
/**
* This file implements styling for menus i.e we have
* 1. Traditional menus and
* 2. Tabbed menus
*/
.menu {
padding:8px;
border:1px solid #CAD5E0 ;
@ -45,3 +50,32 @@
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;
}

@ -2,6 +2,13 @@
/**
* components: search
*/
/* .search-box {
display:grid;
grid-template-columns: auto 64px; gap:4px;
} */
.search .frame .suggestion-frame {
width:98%;
overflow: hidden;

@ -0,0 +1,18 @@
.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;
}
.editor {
background-color:#f3f3f3;
color:#000000;
}
.editor .keyword {color: #4682b4; font-weight:bold}
.code-comment { font-style: italic; color:gray; font-size:13px;}

@ -7,6 +7,8 @@
margin-left:10%;
margin-right:10%;
gap:4px;
}
.main .header { height:64px;
display:grid;

@ -8,10 +8,11 @@
.main .header {
display:grid;
grid-template-columns: 64px auto; gap:4px;
align-items: center;
}
.main .header .title {font-weight:bold; font-size:24px; text-transform: capitalize;}
.main .header .subtitle {font-size:14px; text-transform: capitalize;}
.main .header img {height:64px; width:64px}
.main .menu {display:none}
.main .menu {margin-left:200px}
.pane {width:50%; height:100px; background-color: pink;}

@ -12,43 +12,32 @@ bootup.CMSObserver = function(_sysId,_domId,_fileURI){
var http = HttpClient.instance()
http.setHeader('uri',_fileURI)
if (sessionStorage[_sysId] != null){
if (sessionStorage[_sysId] != null ){
var uri = sessionStorage[_sysId]+'/page'
}else{
var uri = '/page'
}
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){
// console.log(jx.dom.exists(_domId))
// var _domElement = jx.dom.get.instance('div')
// _domElement.className = 'busy-and-loading'
// jx.dom.append(_domId, _domElement)
if (x.status == 200){
// jx.dom.set.value(_domId,x.responseText)
// var _domElement = jx.dom.get.instance('div')
// _domElement.innerHTML = x.responseText
setTimeout(function(){
// _domElement.innerHTML = x.responseText
// _domElement.className = null
// $(_domElement).html(x.responseText)
_content = $(x.responseText)
var _id = $(_content).attr('id')
_pid = (['#',_domId,' #',_id]).join('')
if( $(_pid).length != 0){
$(_pid).remove()
}
$('#'+_domId).append(x.responseText)
// $(_domElement).attr('class',_domId)
//
// If there is a script associated it must be extracted and executed
// menu.runScript(_domId)
// console.log([_domId, ' **** ',$(_domId + ' script')])
},1500)

@ -52,7 +52,7 @@ menu.apply = function (uri,id,pid,_context){
}
menu.apply_link =function(_args){
menu.apply_link =function(_args,_context){
//
// type:
// redirect open new window
@ -75,7 +75,7 @@ menu.apply_link =function(_args){
http.setHeader('uri',_args.uri)
http.setHeader('dom',(_args.title)?_args.title:'dialog')
// http.setHeader('dom',_args.text)
http.get('/dialog',function(x){
http.get(_context+'/dialog',function(x){
jx.modal.show({html:x.responseText,id:'dialog'})
console.log([$('.jxmodal')])
@ -106,4 +106,263 @@ menu.runScript = function(_id){
}
})
}
menu.events = {}
menu.events._dialog = function (_item,_context){
// var url = _args['url']
_item.type = (_item.type == null)? 'redirect' :_item.type
var http = HttpClient.instance()
http.setHeader('uri',_item.uri)
http.setHeader('dom',(_item.title)?_item.title:'dialog')
// http.setHeader('dom',_args.text)
http.get(_context+'/dialog',function(x){
jx.modal.show({html:x.responseText,id:'dialog'})
// menu.events.finalize ('.jxmodal')
})
}
menu.events._open = function (id,uri,_context){
id = id.replace(/ /g,'-')
var pid = '#content'
$('.content').children().slideUp()
$('#'+id).remove()
var httpclient = HttpClient.instance()
_context = (_context == null)?'':_context;
httpclient.setHeader('uri',uri)
httpclient.setHeader('dom',id)
httpclient.post(_context+'/page',function(x){
var _html = x.responseText
var _dom = $(_html)
if(jx.dom.exists(pid) && jx.dom.exists(id)){
jx.dom.remove(id)
}
$(pid).append(_dom)
// jx.dom.append(pid,_dom)
// $('#'+id).show('fast',function(){
// $('#'+pid).slideUp()
// })
var ref = pid + ' #'+id
$(pid).children().slideUp('fast', function(){
$(ref ).slideDown('fast',function(){
$(pid).slideDown('fast',function(){
var input = $(pid).find('input')
if (input.length > 0 ){
$(input[0]).focus()
}
})
})
})
menu.events.finalize (ref)
// $('.content').append(_dom)
})
}
menu.utils = {}
menu.utils.format = function(text){
return text.replace(/(-|_)/g,' ').trim()
}
menu.events.finalize = function (ref) {
var scripts = $(ref+' script')
jx.utils.patterns.visitor(scripts,function(_item){
if(_item.text.trim().length > 0){
var _code = eval(_item.text)
var id = ref
if (_item.parentNode != null){
var id = _item.parentNode.id == null?_item.parentNode.className : _item.parentNode.id
}
id = (id != null)?id : ref
// _delegate.scripts[id] = _code
}
})
}
/**
* Let's build the tab handling here ...
*
*/
var QCMSBasic= function(_layout,_context,_clickEvent) {
this._layout = _layout
this._context= _context
this._make = function (_items){
var _panes = []
var _context = this._context ;
_items.forEach(_item=>{
var _div = jx.dom.get .instance('DIV')
_div.innerHTML = menu.utils.format(_item.text)
//
// We need to check for the override text and see if it goes here
_div.className = 'active'
_div.data = _item
_panes.push(_div)
$(_div).on('click', function (){
//
// how do we process this ...
if(this.data.uri) {
if (this.data.type == 'dialog') {
menu.events._dialog(this.data,_context)
}else{
menu.events._open(menu.utils.format(this.data.text),this.data.uri,_context)
}
}else{
window.open(this.data.url,menu.utils.format(this.data.text))
}
})
})
return _panes ;
}
this.init = function (){
var _me = this ;
var _make = this._make
var _layout = this._layout
var _names = _layout.order.menu.length > 0 ? _layout.order.menu : Object.keys(_layout.menu)
// Object.keys(this._layout.menu)
_names.forEach(function(_name){
var _div = _me._make(_layout.menu[_name]) ;
var _sub = jx.dom.get.instance('DIV')
var _menuItem = jx.dom.get.instance('DIV')
_menuItem.innerHTML = menu.utils.format (_name)
_sub.className = 'sub-menu border-round border '
_menuItem.className = 'item'
_div.forEach(_item=>{$(_sub).append(_item) })
$(_sub).append(_div)
_menuItem.appendChild(_sub)
$('.main .menu').append(_menuItem)
})
}
}
var QCMSTabs = function(_layout,_context,_clickEvent){
//
// This object will make tabs in the area of the menu
// @TODO: we can parameterize where the menu is made to improve flexibility
//
this.tabs = jx.dom.get.instance('DIV')
this.tabs.className = 'tabs'
this._context = _context
this._make = function (text,_item,_event){
var text = text.trim().replace(/(_|-)/ig,' ').trim()
var _context = this._context;
if (text.match(/\//)){
text = text.split(/\//g).slice(-1)[0]
}
var _button = jx.dom.get.instance('INPUT')
var _label = jx.dom.get.instance('LABEL')
_button.type= 'radio'
_button.id = text+'tab'
_button.name = 'menu-tabs'
_label.innerHTML = text.toLowerCase()
_label._uri = _item[0].uri
_label.htmlFor = _button.id
$(_label).on('click',function (){
menu.events._open(this.innerHTML,this._uri,_context)
})
// $(this.tabs).append( [_button,_label])
return [_button,_label]
}
this._layout = _layout
this.init = function (){
var _me = this;
var _make = this._make
var tabs = this.tabs
var _names = _layout.order.menu.length > 0 ? _layout.order.menu : Object.keys(_layout.menu)
// Object.keys(_layout.menu).
_names.forEach(function(_key){
_item = _layout.menu[_key]
// console.log([_item])
_tabItem = _me._make(_key,_item)
$(tabs).append(_tabItem)
})
this.tabs.className = 'tabs'
$('.main .menu').append(this.tabs)
$('.main .menu').css({'border':'1px solid transparent'})
}
//
// We need to load the pages here ...
//
}
menu.tabs = { }
// menu.tabs.make = function(text,_clickEvent){
// var _id = text.trim()
// if (text.match(/\//)){
// _id = text.split(/\//g).slice(-1)[0]
// }
// var _button = jx.dom.get.instance('div')
// var _label = jx.dom.get.instance('LABEL')
// _button.type= 'radio'
// _button.id = _id
// _label.innerHTML = _id.toLowerCase()
// $(_label).on('click',_clickEvent)
// return [_button,_label]
// }
menu.tabs.init =function (_layout,_context){
//
// Let us determine what kind of menu is suited for this
// @TODO: Make menus configurable i.e on other areas of the site
//
if (_layout.order != null){
if (_layout.order.length == null && _layout.order.menu == null){
_layout.order = {menu:[]}
}else if (_layout.order.menu == null){
_layout.order.menu = []
}
}else{
_layout.order = {menu:[]}
}
var _count = 0
var _items = 0
Object.keys(_layout.menu).forEach(_name=>{
_items += _layout.menu[_name].length
_count += 1
})
if (_count == _items){
var _menuObject = new QCMSTabs (_layout,_context)
}else{
var _menuObject = new QCMSBasic (_layout,_context)
}
// console.log(_layout)
// var _tabs = new QCMSTabs (_layout)
console.log(_menuObject)
_menuObject.init()
}

@ -1,11 +1,9 @@
{% if layout.header.logo == True %}
<div class="icon">
<img src="{{system.icon}}">
</div>
<div class="icon">
<img src="{{system.icon}}">
</div>
{% endif %}
<div>
<div class="title">{{layout.header.title}}</div>
<div class="subtitle">{{layout.header.subtitle}}</div>
</div>
<div>
<div class="title">{{layout.header.title}}</div>
<div class="subtitle">{{layout.header.subtitle}}</div>
</div>

@ -23,24 +23,37 @@ Vanderbilt University Medical Center
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="quick cms, cms, python, flask, qcms">
<meta name="robots" content="/, follow, max-snippet:-1, max-image-preview:large">
<link href="{{system.context}}/static/css/default.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/static/css/menu.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/static/css/border.css" rel="stylesheet" type="text/css">
<!-- <link href="{{system.context}}/static/css/default.css" rel="stylesheet" type="text/css"> -->
<!-- <link href="{{system.context}}/static/css/menu.css" rel="stylesheet" type="text/css"> -->
<!-- <link href="{{system.context}}/static/css/border.css" rel="stylesheet" type="text/css"> -->
<!-- <link href="{{system.context}}/static/css/animation/_ocean.css" rel="stylesheet" type="text/css"> -->
<link href="{{system.context}}/static/css/themes/{{system.theme}}" rel="stylesheet" type="text/css">
<link href="{{system.context}}/static/css/icons.css" rel="stylesheet" type="text/css">
<script src="{{system.context}}/static/js/jx/dom.js"></script>
<script src="{{system.context}}/static/js/jx/utils.js"></script>
<script src="{{system.context}}/static/js/jx/rpc.js"></script>
<script src="{{system.context}}/static/js/jx/ext/modal.js"></script>
<script src="{{system.context}}/static/js/jx/ext/math.js"></script>
<script src="{{system.context}}/static/js/jquery/jquery.js"></script>
<script src="{{system.context}}/static/js/menu.js"></script>
<script src="{{system.context}}/static/js/search.js"></script>
<script src="{{system.context}}/static/js/bootup.js"></script>
<script src="{{system.context}}/static/js/fontawesome/js/all.js"></script>
<!-- <link href="{{system.context}}/static/css/themes/{{system.theme}}" rel="stylesheet" type="text/css"> -->
<!-- <link href="{{system.context}}/static/css/icons.css" rel="stylesheet" type="text/css"> -->
<link href="{{system.parentContext}}/static/css/icons.css" rel="stylesheet" type="text/css">
<link href="{{system.parentContext}}/static/css/source-code.css" rel="stylesheet" type="text/css">
<link href="{{system.parentContext}}/static/css/search.css" rel="stylesheet" type="text/css">
<link href="{{system.parentContext}}/static/css/dialog.css" rel="stylesheet" type="text/css">
<!-- applying themes as can -->
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/layout.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/header.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/menu.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/borders.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/footer.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/pane.css" rel="stylesheet" type="text/css">
<script src="{{system.parentContext}}/static/js/jx/dom.js"></script>
<script src="{{system.parentContext}}/static/js/jx/utils.js"></script>
<script src="{{system.parentContext}}/static/js/jx/rpc.js"></script>
<script src="{{system.parentContext}}/static/js/jx/ext/modal.js"></script>
<script src="{{system.parentContext}}/static/js/jx/ext/math.js"></script>
<script src="{{system.parentContext}}/static/js/jquery/jquery.js"></script>
<script src="{{system.parentContext}}/static/js/menu.js"></script>
<script src="{{system.parentContext}}/static/js/search.js"></script>
<script src="{{system.parentContext}}/static/js/bootup.js"></script>
<script src="{{system.parentContext}}/static/js/fontawesome/js/all.js"></script>
</head>
<script>
sessionStorage.setItem('{{system.id}}','{{system.context|safe}}')
@ -56,8 +69,8 @@ Vanderbilt University Medical Center
</script>
<body>
<div class="main">
<div id="header" class="header" onclick="window.location.href='{{system.context}}/'" style="cursor:pointer">
<div class="main {{system.theme}}">
<div id="header" class="header" onclick="window.location.href='{{system.parentContext}}'" style="cursor:pointer">
{%include "header.html" %}
</div>

@ -1,7 +1,7 @@
{%if system.portal %}
<div class="icon active">
<div align="center" class="button" onclick="window.open('{{system.context}}/set/main','_self')" style="display:grid; grid-template-columns:auto auto; gap:4px; align-items:center ">
<div align="center" class="button" onclick="window.open('{{system.parentContext}}/','_self')" style="display:grid; grid-template-columns:auto auto; gap:4px; align-items:center ">
<i class="fa-solid fa-chevron-left" style="color:darkgray; display:block"></i>
<img src="{{system.caller.icon}}" style="height:100%"/>
</div>
@ -11,33 +11,7 @@
<i class="fa-solid fa-home"></i>
</div>
{% endif %}
{% for _name in layout.menu %}
<div class="item">
<div>
<i class="{{layout.icons[_name]}}"></i>
{{_name.replace('-', ' ').replace('_',' ')}}
</div>
{%if layout.menu[_name] %}
<div class="sub-menu border-round border">
{% for _item in layout.menu[_name] %}
{%if _item.type == 'open' %}
<div class="active" onclick="window.open('{{_item.uri}}','_self')">
{%elif _item.uri and _item.type not in ['dialog','embed'] %}
<div class="active" onclick="menu.apply('{{_item.uri}}','{{_item.text}}','{{_name}}','{{system.context}}')">
{% else %}
<!-- working on links/widgets -->
<div class="active" onclick='menu.apply_link({{_item|tojson}})'>
{% endif %}
<i class="fa-solid fa-chevron-right" style="margin-right:4px"></i>
{{_item.text.replace('-',' ').replace('_',' ')}}
</div>
{%endfor%}
</div>
{%endif%}
</div>
{%endfor%}
<script>
menu.tabs.init({{layout|tojson}},'{{system.context}}')
</script>

@ -1,4 +1,15 @@
__author__ = "Steve L. Nyemba<steve@the-phi.com>"
__version__= "2.0"
__author__ = "Steve L. Nyemba"
__version__= "2.1.6"
__email__ = "steve@the-phi.com"
__license__="""
Copyright 2010 - 2024, Steve L. Nyemba, Vanderbilt University Medical Center
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Software), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""

@ -0,0 +1,44 @@
### What is QCMS
QCMS is a lightweight, flask CMS designed easy to get working and allowing users to be able to change/edit content using **nextcloud** or **owncloud** as they see fit if need be.
QCMS comes with standard web frameworks and python frameworks out of the box
|HTML/JS| Python Frameworks|
|---|---|
|FontAwesome<br>JQuery<br>ApexCharts<br>&nbsp;| Flask<br>Numpy<br>Pandas<br>data-transport
**Features**,
**QCMS** Lowers the barrier to entry by :
1. automatically building menus from folder structure
2. add content in HTML5 or Markdown format
### Quickly Getting Started
**1. Installation**
pip install dev.the-phi.com/git/qcms/cms
**2. Create your project**
qcms create --title "my demo project" --subtitle "I love qcms" --port 8090 ./qcms-demo
The content is created in ./qcms-demo/www/html
**3. Boot the project**
qcms bootup ./qcms-demo/qcms-manifest.json
### Limitations
- Can not yet migrate an existing project into **QCMS**
- Templates are all CSS/SCSS based (no Javascript)
Things to update with qcms runner :
- system.icon location, when creating a project
- applying themes
- upgrade all plugins to return data-type ?
Loading…
Cancel
Save