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 json
import importlib import importlib
import importlib.util import importlib.util
from git.repo.base import Repo # from git.repo.base import Repo
import typer import typer
from typing_extensions import Annotated from typing_extensions import Annotated
from typing import Optional from typing import Optional
from typing import Tuple
import meta import meta
import uuid import uuid
from termcolor import colored from termcolor import colored
import base64 import base64
import io import io
import cms
from cms import index 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 start = index.start
The basic template for a flask application will be created in a specified location, within this location will be found a folder "content" __doc__ = f"""
- within the folder specify the folder structure that constitute the menu Built and designed by Steve L. Nyemba, steve@the-phi.com
Usage : version {meta.__version__}
qcms --create <path> --version <value> --title <title> --subtitle <subtitle>
""" {meta.__license__}"""
PASSED = ' '.join(['[',colored(u'\u2713', 'green'),']']) PASSED = ' '.join(['[',colored(u'\u2713', 'green'),']'])
FAILED= ' '.join(['[',colored(u'\u2717','red'),']']) FAILED= ' '.join(['[',colored(u'\u2717','red'),']'])
@ -40,94 +43,6 @@ INVALID_FOLDER = """
# handling cli interface # handling cli interface
cli = typer.Typer() 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") @cli.command(name="info")
def _info(): def _info():
@ -135,23 +50,23 @@ def _info():
This function returns metadata information about this program, no parameters are needed This function returns metadata information about this program, no parameters are needed
""" """
print () print ()
print (meta.__name__,meta.__version__) print ('QCMS',meta.__version__)
print (meta.__author__,meta.__email__) print (meta.__author__,meta.__email__)
print () 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: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", 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)")]="", 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, 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): 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 global INVALID_FOLDER
_config = _get_config() _config = config.get()
if _config : if _config :
_app = _config['system']['app'] _app = _config['system']['app']
_app['host'] = host _app['host'] = host
@ -159,21 +74,18 @@ def set_app (host:Annotated[str,typer.Argument(help="bind host IP address")]="0.
_app['debug'] = debug _app['debug'] = debug
_config['system']['context'] = context _config['system']['context'] = context
_config['app'] = _app _config['app'] = _app
write_config(_config) config.write(_config)
_msg = f"""{PASSED} Successful update, good job ! _msg = f"""{PASSED} Successful update, good job !
""" """
else: else:
_msg = INVALID_FOLDER _msg = INVALID_FOLDER
print (_msg) 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:str): #url:str,uid:str,token:str):
def set_cloud(path:Annotated[str,typer.Argument(help="path of the auth-file for the cloud")]): #url:str,uid:str,token:str): def set_cloud(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 Setup qcms to generate a site from files on nextcloud
The path must refrence auth-file (data-transport)
:url url of the nextcloud
:uid account identifier
:token token to be used to signin
""" """
if os.path.exists(path): if os.path.exists(path):
f = open (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'] url = _auth['url']
if os.path.exists('qcms-manifest.json') : 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}} _config['system']['source'] = path #{'id':'cloud','auth':{'url':url,'uid':uid,'token':token}}
write_config(_config) config.write(_config)
title = _config['layout']['header']['title'] title = _config['layout']['header']['title']
_msg = f""" _msg = f"""{PASSED} Successfully update, good job!
Successfully update, good job! {url}
{url}
""" """
else: else:
_msg = INVALID_FOLDER _msg = INVALID_FOLDER
@ -197,13 +108,15 @@ def set_cloud(path:Annotated[str,typer.Argument(help="path of the auth-file for
else: else:
_msg = INVALID_FOLDER _msg = INVALID_FOLDER
print (_msg) print (_msg)
@cli.command(name='set-key') @cli.command(name='secure')
# def set_key(path): # def set_key(path):
def set_key( def secure(
manifest:Annotated[str,typer.Argument(help="path of the manifest file")], manifest:Annotated[str,typer.Argument(help="path of the manifest file")],
keyfile:Annotated[str,typer.Argument(help="path of the key file to generate")]): 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): if not os.path.exists(keyfile):
@ -211,11 +124,11 @@ def set_key(
f.write(str(uuid.uuid4())) f.write(str(uuid.uuid4()))
f.close() f.close()
# #
_config = _get_config(manifest) _config = config.get(manifest)
if 'source' not in _config['system']: if 'source' not in _config['system']:
_config['system']['source'] = {'id':'disk'} _config['system']['source'] = {'id':'disk'}
_config['system']['source']['key'] = keyfile _config['system']['source']['key'] = keyfile
write_config(_config,manifest) config.write(_config,manifest)
_msg = f"""{PASSED} A key was generated and written to {keyfile} _msg = f"""{PASSED} A key was generated and written to {keyfile}
use this key in header to enable reload of the site ...` 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 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') @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")], 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', 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', 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', 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', 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',
version:str=typer.Option(default="0.2",help="version number") #Annotated[str,typer.Argument(help="version of the site")]='0.1' 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 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,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAANpElEQVR4Xu2dB9AlRRHHwZxzjt+Zc0KhzIeKEXMqA/CZoBTFrBjQExVUTJhKVPQUscyomBU9M+acMB0mTKhlzvj/Vb0t11e7b2fmzU7uqq7b+96k7fnvhJ6e7l13aVS1BHat+u3by+/SAFA5CBoAGgAql0Dlr99GgAaAyiVQ+eu3EaABoHIJVP76tY0A11R/P1J8I/FFxD8Tv098pPhHNWKhJgA8VB38AvEZBjr6z/rbPuLjagNBTgDg6z1J/FeHTrqD8rxDvOp9/6nfry/+okP52WbJBQBnk4S/L/63eJt4++LZRPCnV6Ifii9lkHiH0uxpkK6YJLkA4ImS+DN7Uv+WnvnbOw164sZK83GDdF2Si+nhFIv0WSfNAQDnl4R/ID73gKQ/rb89XvzJFb3wAP32Kote2qq0H7NIn3XSHADwPEn4URNSPn4xInxjIB2Lu9dZ9NINlPYzFumzTpo6AC4t6X5HfBYDKbM+OEb8VPGPe+mvoudvGuQnyd/FFxD/yTB99slSB8B2SXg/Syn/TelfKj5cfOoiL4vALQblvEZp7m+QrpgkKQPg6pLyl8Ws4l3o98rE9PE9MUCaGkV+ojTX7oHGpc7s8qQMAOb1vQNJ9HOq5+7i/tQRqOq41aQKgJtILHOvxNH+HSt+l/i94tPidkWc2lMFANs7tHJz0tNU+LY5K8ih7BQBsLsE91zxucSXF6MFnIPYXdxL/JU5Cs+lzBQB0JfdmfSfvcRPEN9wBqH+R2W+UXyImJ1CdZQ6ALoOOZ0eHiE+Qsyzb/qHCjxK/Azxr3wXnnJ5uQCgk+EBenj5jAL9o8pm6whXoQzKDQD0/dvEd5kRBBTNKMBowKjA6FAs5QiAK6s3UO2GaDt6AU4hjxajai6OQghxDqF9QYXuNkfBI2WikWQh+oGAdQapKlcAsA5gPRCaTlCFtxNzaFQE5QoADnoOjtADb1ad94xQ72xV5goArHgPmk0qwwVjM3g1MXaJxVCuAOCcgPOCkMS08+CQFYaoKyYAqNvlAOYSyocN/5B591wy4+AItXRxtoIxAYCZ1v5iDDhsCGOPh9hk8JCWreCTPZSTXBGxAMBK+t3iV4htVvMm9v2+hfwbFXhZ8R98F5xCeTEAgC6fEzgsfiAWdI8R/2tCIBhsYLJ19sCC4yrZCwPXGay6GADYV2/32qU35DYO5/Pc01sGAkChEzbFodu7U3VeSVzMvn8ZWaEFemY1gHP4jRGI/0J//6r4p2Ksc1l4YdUbiwArlsbFUmgA8CU/PxNpfl3tvJYYm4FiKSQAzikpcr/vQplI87ZqJ1NS0RQSAByvPikTaXKX8KaZtHWtZoYCAM4Y+PpdVvCYaqEzYO2AQwdGEI6E7ytmgTYHYZB64hwFp1ZmKAC4KG/Yd6N6xWZvbB6+s357mRiA+SJ0/ucTYxHERREWon8Rszjl8khRFAIAl5PEuM59RgvJYYhxKzFf/RShGt4hRlnjixh1UDX3fQoAQuwBOIRiNCuCQgBgiySFjR1fqwmhB9gq/pRJ4kWaK+hflEtntcjjmvS3yngzMSNC9hQCAJ2QmFefJZ46xUPrxnbRllAkPcU2k2N6RiZc1mRvLxgSAJ2sOQfAoKNTBS/3wWX0BxePXXj2YOpwvUxqiwUMQzAQyZpiAACBcR7AKv5QMT4AOsITCGsGV/raCmC5ljmWz/Ygy3f9XsqLBYCu8aiGWemjH0D1+yHxLdd4M1eTcerFFPw+FnW/XWnvapE+yaSxAdAJhXuAjxVfVbyOzf9bHTqFrR2LSObzk8XsKkyI7eeBJglTTpMKADoZsYp38QPY5f+SHti72xBOpF69yPBK/ftAw8zsavA9mDWlBoB1hMkUgqaQC6WmhH7iGuLu0gfrDxxNMTWtIoC2h3jKhsG0HdHSlQSAx0mKz7aU5NBXfG+Vgep5bDfBUfWe4iKUQaUA4JLqEI5vh3wJjmECJxRjV85xJs2x9fV6mVkjsMbAZd0vLYGWbPISAIDe/qOLodxG0CikPjGRAfUyB07cGmZqQAtYFOUOAL5UzMtQHtkQBqm3t8lQatqcAcAcjb9gto8Ym5gShzpY+jBlVE85A6DrPHwJA4KHi6d8AZKHEWOz+p5fCKAEAHR9yUKQyxvs68dW8CzkMCap0h/QEOhLAkD3flgRbxNzj2CZWNk/un39/5NAiQDo3g6v3xw/Ey8AYiWPoqcqJ1BTYC8ZAN2730IPfPlvEveDTkzJporfawAAHYl5F+uCYm/4uKK1FgC4yqf4fA0AxXfx6hdsAGgAqFwClb9+aiPAOdQfrNo3xLhl+awYO79UCIOVmy+2k9gC4K+QYBPZXiBNBQC0A2fQBHxaPtIlJBxWOt+NjIJN1f8c8QWX2gFAH7QAQuQm2lefCgBerKYT23eMfqcfUOiYRv+yl8TqHHgJPWxFEvwccZPJJkCl7zY6lZcCADiWJWzLFPGlXUcc2mcvRiFMRVOywhwN41LuEWZDUy8V4kV2qBLTq9i3Udr3h2hUr4436JnIIibUNzA1SR89TWwAoKHDCtjU5x83irABCEl82dw6MqHsjppjA4AFH/H9TCmGt06uiZv6NSCY9Z1MXyaFdLEBgAzwA2Bq0cMNolWLsTlkykXQKxoW/BKle5hh2iSSpQAAmzmWG7mh9QJEMDO1Ibi10mYVUyAFAGB1S0CGKXOutyjNPSJ8NhdVnWw/zztRNxHHMVLNSimUAgCQK6tsFlBjXkTQuLHPjmWWzYXV48RjMQyJT4yGMDsXMqkAABCwuh8y2MCSZyNi53cf/v300N0hXB4MaN/JEUantav0CQC+DldnTRhr4N0DnwHLhBEHyiJ8B7gQhqBc5+LOoOnN3+V6kBOaStTVQ4SzCEYpF2LK2OmS0UcenwBASUMQ5tSIRSOLRzjFMLGoubndFIUaAKKI/f8qbQCYuQ/aCLBCwG0EmBl9BsW3EcBASOskaSNAGwHaInAMA20KWGds8ZO3TQF+5DhaSpsC2hTQpoA2BTRF0CAG2hpg5vnHoPii1gAYd2IVgxtV3KljIIn/vuuKOfHrrmobyMVbkv4aABt+PH3RRlzEcPeA9uEy5m5i1NmhqRgA4HkD275VvncQMMEfLxxQyh0ACDWDCRpHt2OETwF8BPoMPjH1qsUAYOpFu9+JwoH9fN9LuGlel3QdALq8F9cDsYf3EmPswUnh8WLMuTBPw+fQR8R4EA1BywDYoko5ecS+gNGJWIpYQr9I7N25hc81gI2wGHIZjm3CyNiU30/bBwDHynzh5xko7Of62x3FHOtuiJnCcGI9N2EU21kbcRx+lHjI8ASgYBH1YZ8NigUA3sElkJTLu5+qTAzpHAdjr7fK9IxA0Vw+wbIHh1NPd6nQIQ/rI46E+cpXBbxgzULkFW8u7mICIOT5PHcPMAgxiSbCCLGfmHUKowLBLVKiD6oxmMd5oZgA4AXwuZtaJFG+MtYBWCJhQAJQUyIsiFi7eFkPxAYA8+1uKUl30RbuAZwkZluboktZEz/HRmKNDYAUvzAEx5oBZ5KYuMXQDUx1HttVzNDXppgAYG5l0TVlb7/2S1oWwGqbaQkHEASUQL+REhHZlPbZXKkbbX9MAIBim+CQ63TCTmVmlW2yrTtS6bD+3RC7hK9bp50meQlVhxWyF4oJAGICe3uRFdJAkYLyiRjAeBtZNeLQ4aituYCCc0mXAJa2HcOijo+BdqGmXnUR9deL9hEf0QvFAgBaOPbkIervK4J2V514DN0YkB7X0wgDBwgIPMUcOxU7yEcn9BVBW1XgseKh6+iEqCGimjcdAI0P0QHLQkLRQpy+ULbwy6pgvjAUL50qmK+JABLcPcT7CCMF2rZ+4GgfHT1WRh8ApOG8gviFqIKZ608R87G8Xuzd06lPALCdY3+KMIcIte/+Yhwtjd2x+7Z+45yAl8ZjGKtxOopnV+oAwK0gOpfwMkPEopSAUawBxsDJ6MA5AecHjA5EKqF96yxklwHg+p5O+XwCoLsZxFDKRUq2eNzrA8X42eG4dWOklTv0d6J+fX7gd1yzcXhziNgmKFRX1LJJ2In6gXh/DKW0D69fjEoM/9xUHiLCxBGUgs5fJoC9KUZt7HLKWRwARmQ4+ucj9MvB4qlr1XQOo4vtUe26NoHcWj5APDX8csrIqaJt4MqqAXC0BGYaqRME4YWLL9hmyF0HAO9RXZwQmnomYwRgFCN6iSlVCwDmeeZkdO82xFk5fgVNyRUAtIv20U4bIhglFlGmVC0AGPZtI30iVOZcOoUDGxNyBQAgO8ikgoE0NhrEagGAihUHTC7Eke0+hhldAcDq3tX4And2ANyEqgQAcyrbKNO5dVmQY95EhgTuCgC2jfgIdCHsCbYbZqwSAGy/TPTyYzI8UD9gw2dCrgBgy4mNoAvhK5CtsAlVCYDTJBmUO65+dQ9VXvQCJuQKABaArhHC2Tbi1NKEqgQAgiEuwAkmEhpIg1kUc7QJuQJgXxV+jEkFA2kwfd80zFstABAuQrYl9tgYa5j6F3YFAOAEpLaE11NUxaZTXLUAwOBiDzFqVhtCM2cDHFcA0Ka9xSiDbMhmB0C5xQDARkgtbSIS8HkYlMgrtWbYSKABwEZaBaZtACiwU21eqQHARloFpm0AKLBTbV6pAcBGWgWmbQAosFNtXqkBwEZaBab9L2FjSp+s5clTAAAAAElFTkSuQmCC"""
_,_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) # Build the configuration for the project
_config['system']['logo'] = os.sep.join([_config['layout']['root'],'_images/logo.png']) 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') @cli.command (name='reload')
def reload (path) : def reload (
if os.path.exists (path): path:Annotated[str,typer.Argument(help="")],
pass 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") @cli.command(name="bootup")
def 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)") 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 This function will launch a site/project given the location of the manifest file
""" """
index.start(path,port) if not manifest.endswith('json') and os.path.isdir(manifest):
# if not port : manifest = manifest if not manifest.endswith(os.sep) else os.sep.join(manifest.split(os.sep)[:-1])
# index.start(path) manifest = os.sep.join([manifest,'qcms-manifest.json'])
# else: index.start(manifest,port)
# index.start(path,port) @cli.command(name='theme')
def handle_theme (
def reset(): 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 global SYS_ARGS
if __name__ == '__main__': if __name__ == '__main__':
cli() cli()

@ -6,192 +6,62 @@ import copy
from jinja2 import Environment, BaseLoader, FileSystemLoader from jinja2 import Environment, BaseLoader, FileSystemLoader
import importlib import importlib
import importlib.util 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()
# 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()
# #return ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>']) # def _system(**_args):
# _html = ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>']) # """
# appContext = Environment(loader=BaseLoader()).from_string(_html) # Both version and context must be provided otherwise they are ignored
# # # :version
# # If the rendering of the HTML happens here we should plugin custom functions (at the very least) # :context context
# # # """
# _info = _isvalid(['version','context'],**_args)
# return appContext.render(**_args) # _info['theme'] = 'default.css'
# # return _html # _info = _info if _info else {'version':'0.0.0','context':'','theme':'default.css'}
# @staticmethod # if _info :
# def data (_args): # _info['logo'] = None if 'logo' not in _args else _args['logo']
# """ # #
# :store data-store parameters (data-transport, github.com/lnyemba/data-transport) # # There is an aggregation entry here in app
# :query query to be applied against the store (expected data-frame) # _appInfo = {'debug':True,'port':8084,'threaded':True,'host':'0.0.0.0'}
# """ # if 'app' not in _args:
# _store = _args['store'] # _info['app'] = _appInfo
# reader = transport.factory.instance(**_store) # else:
# _queries= copy.deepcopy(_store['query']) # _info['app'] = dict(_appInfo,**_args['app'])
# _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 : # return _info
# _map = dict(_map,**_plugins) # def _header(**_args):
# return _map # return _isvalid(['logo','title','subtitle'],**_args)
# @staticmethod # def _layout(**_args):
# def context(_config): # _info = _isvalid(['root','index'],**_args)
# """ # _info['on'] = {"load":{},"error":{}}
# adding custom variables functions to Jinja2, this function should be called after plugins are loaded # _url = 'qcms.co'
# """ # _overwrite = {"folder1":{"type":"redirect","url":_url},"folder2":{"type":"dialog"},"folder3":{"type":"dialog","url":_url}}
# _plugins = _config['plugins'] # _info['icons'] = {"comment":"use folder names as keys and fontawesome type as values to add icons to menu"}
# # if not location: # _info["api"] = {"comment":"use keys as uri and function calls as values"}
# # env = Environment(loader=BaseLoader()) # _info['map'] = {},
# # else: # _info['order'] = {'menu':[]}
# location = _config['layout']['root'] # _info['overwrite'] = _overwrite
# # env = Environment(loader=FileSystemLoader(location)) # return _info
# 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

@ -128,13 +128,15 @@ def html (uri,_config) :
_html = _handler.get_file_contents(uri).decode('utf-8')#.replace('.attachments.', copy.deepcopy(_link)) _html = _handler.get_file_contents(uri).decode('utf-8')#.replace('.attachments.', copy.deepcopy(_link))
# print ([uri,uri[-2:] ,uri[-2:] in ['md','MD','markdown']]) # print ([uri,uri[-2:] ,uri[-2:] in ['md','MD','markdown']])
_handler.logout() _handler.logout()
# if uri.endswith('.md'): # if uri.endswith('.md'):
if not _context : if f"{_root}{os.sep}" in _html :
_html = _html.replace(_root,('api/cloud/download?doc='+_root)).replace('.attachments.', copy.deepcopy(_link)) if not _context :
else: _html = _html.replace(_root,('api/cloud/download?doc='+_root)).replace('.attachments.', copy.deepcopy(_link))
_html = _html.replace(_root,(f'{_context}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 />','') # _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): # def update (_config):
# """ # """
# This function updates the configuration provided by loading default plugins # This function updates the configuration provided by loading default plugins

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

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

@ -20,7 +20,7 @@ class Initializer :
""" """
def __init__(self,**_args): def __init__(self,**_args):
self._config = {'system':{},'layout':{},'plugins':{}} 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._location= _args['location'] if 'location' in _args else None
self._menu = {} self._menu = {}
# _source = self._config ['system']['source'] if 'source' in self._config['system'] else {} # _source = self._config ['system']['source'] if 'source' in self._config['system'] else {}
@ -29,8 +29,13 @@ class Initializer :
self._ISCLOUD = False self._ISCLOUD = False
self._caller = None if 'caller' not in _args else _args['caller'] self._caller = None if 'caller' not in _args else _args['caller']
self._args = _args self._args = _args
# if 'context' in _args :
# self._config
self.reload() #-- this is an initial load of various components 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): def reload(self):
@ -39,7 +44,7 @@ class Initializer :
self._isource() self._isource()
self._imenu() self._imenu()
self._iplugins() self._iplugins()
self._iroutes ()
# self._ISCLOUD = 'source' in self._config['system'] and self._config['system']['source']['id'] == 'cloud' # self._ISCLOUD = 'source' in self._config['system'] and self._config['system']['source']['id'] == 'cloud'
@ -52,7 +57,15 @@ class Initializer :
return cloud return cloud
else: else:
return disk 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) : def _imenu(self,**_args) :
pass pass
def _iplugins(self,**_args) : def _iplugins(self,**_args) :
@ -252,8 +265,9 @@ class Initializer :
_callerContext = self._caller.system()['context'] _callerContext = self._caller.system()['context']
if not self._config['system']['context'] : if not self._config['system']['context'] :
self._config['system']['context'] = _callerContext self._config['system']['context'] = _callerContext
self._config['system']['caller'] = {'icon': 'caller/main/'+self._caller.system()['icon'].replace(_callerContext,'')} self._config['system']['caller'] = {'icon': '/main'+self._caller.system()['icon'].replace(_callerContext,'')}
_context = _callerContext _context = '/'.join([_callerContext,_context]) if _callerContext != '' else _context
if os.path.exists(_newpath) and not self._ISCLOUD: if os.path.exists(_newpath) and not self._ISCLOUD:
@ -278,14 +292,15 @@ class Initializer :
else: else:
_icon = f'{_context}/api/disk/read?uri={_logo}' _icon = f'{_context}/api/disk/read?uri={_logo}'
if disk.exists(uri=_logo,config=self._config): if disk.exists(uri=_logo,config=self._config):
_icon = _logo _icon = _logo
if self._location : if self._location :
self._config['layout']['location'] = _path self._config['layout']['location'] = _path
self._config['system']['icon'] = _icon self._config['system']['icon'] = _icon
self._config['system']['logo'] = _logo self._config['system']['logo'] = _logo
# self.set('layout.root',os.sep.join([_path,_oroot])) # self.set('layout.root',os.sep.join([_path,_oroot]))
pass pass
class Module (Initializer): class Module (Initializer):
@ -312,6 +327,15 @@ class Module (Initializer):
elif type(_stream) == io.StringIO : elif type(_stream) == io.StringIO :
self._config = json.loads( _stream.read()) 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' 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'] # 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): class MicroService (Module):
""" """
This is a CMS MicroService class that is capable of initializing a site and exposing Module functions This is a CMS MicroService class that is capable of initializing a site and exposing Module functions
""" """
def __init__(self,**_args): def __init__(self,**_args):
super().__init__(**_args) super().__init__(**_args)
@ -402,7 +427,6 @@ class CMS:
_system = _system['routes'] _system = _system['routes']
for _name in _system : for _name in _system :
_path = _system[_name]['path'] _path = _system[_name]['path']
self._apps[_name] = MicroService(context=_name,path=_path,caller=_app,location=_path) self._apps[_name] = MicroService(context=_name,path=_path,caller=_app,location=_path)
self._apps['main'] = _app 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 pandas as pd
import uuid import uuid
import datetime import datetime
import requests
from cms import disk, cloud, engine from cms import disk, cloud, engine
_app = Flask(__name__) _app = Flask(__name__)
cli = typer.Typer() cli = typer.Typer()
# @_app.route('/favicon.ico') @_app.route('/<id>/favicon.ico')
# def favicon(): def favicon(id):
# global _route global _route
# _system = _route.get ().system() # _system = _route.get ().system()
# _handler = _route.get() # _handler = _route.get()
_handler = _getHandler(id)
# _logo =_system['icon'] if 'icon' in _system else 'static/img/logo.svg' _system = _handler.system()
# return _handler.get(_logo) _logo =_system['icon'] #if 'icon' in _system else 'static/img/logo.svg'
# # # _root = _route.get().config()['layout']['root'] _stream = requests.get(''.join([request.host_url,_logo]))
# # # print ([_system])
# # # if 'source' in _system and 'id' in _system['source'] and (_system['source']['id'] == 'cloud'): return "_stream",200,{"Content-Type":"image/png"} #_handler.get(_logo),200,{"content-type":"image/png"}
# # # uri = f'/api/cloud/downloads?doc=/{_logo}'
# # # print (['****' , uri]) def _getHandler (app_id,resource=None) :
# # # return redirect(uri,200) #,{'content-type':'application/image'} global _route
# # # else: _id = _getId(app_id,resource)
# # # return send_from_directory(_root, #_app.root_path, 'static/img'),
# # _logo, mimetype='image/vnd.microsoft.icon')
def _getHandler () :
_id = session.get('app_id','main')
return _route._apps[_id] return _route._apps[_id]
def _setHandler (id) : def _getId(app_id,resource):
session['app_id'] = id 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") @_app.route("/robots.txt")
def robots_txt(): def robots_txt():
""" """
@ -74,44 +73,56 @@ def robots_txt():
''') ''')
# return '\n'.join(_info),200,{'Content-Type':'plain/text'} # return '\n'.join(_info),200,{'Content-Type':'plain/text'}
return Response('\n'.join(_info), mimetype='text/plain') 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("/") @_app.route("/")
def _index (): def _index ():
# global _config return _getIndex('main')
# global _route # def _xindex ():
# _handler = _route.get() # _handler = _getHandler()
# _config = _route.config() # _config = _handler.config()
_handler = _getHandler() # global _route
_config = _handler.config()
print ([' serving ',session.get('app_id','NA'),_handler.layout()['root']]) # # print ([' serving ',session.get('app_id','NA'),_handler.layout()['root']])
# _system = _handler.system() # _args={'system':_handler.system(skip=['source','app','data']),'layout':_handler.layout()}
# _plugins= _handler.plugins()
# _args = {} # try:
# # if 'plugins' in _config : # uri = os.sep.join([_config['layout']['root'], _config['layout']['index']])
# # _args['routes']=_config['plugins'] # _index_page = "index.html"
# # _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) # _args = _route.render(uri,'index',session.get('app_id','main'))
# _html = _handler.html(uri,'index') # # _setHandler('main')
_index_page = "index.html" # except Exception as e:
_args = _route.render(uri,'index',session.get('app_id','main')) # # print ()
except Exception as e: # print (e)
# print () # _index_page = "404.html"
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 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>') # @_app.route('/id/<uid>')
# def people(uid): # def people(uid):
# """ # """
@ -132,23 +143,28 @@ def _dialog ():
_args = _route.render(_uri,'html',session.get('app_id','main')) _args = _route.render(_uri,'html',session.get('app_id','main'))
_args['title'] = _id _args['title'] = _id
return render_template('dialog.html',**_args) #title=_id,html=_html) return render_template('dialog.html',**_args) #title=_id,html=_html)
@_app.route("/caller/<app>/api/<module>/<name>")
def _delegate_call(app,module,name): @_app.route("/api/<module>/<name>",defaults={'app':'main','key':None})
global _route @_app.route("/<app>/api/<module>/<name>",defaults={'key':None})
_handler = _route._apps[app] @_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) return _delegate(_handler,module,name)
@_app.route('/api/<module>/<name>') # @_app.route('/api/<module>/<name>')
def _getproxy(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 This endpoint will load a module and make a function call
:_module entry specified in plugins of the configuration :_module entry specified in plugins of the configuration
:_name name of the function to execute :_name name of the function to execute
""" """
# global _config
# global _route _handler = _getHandler( app_id,key)
# _handler = _route.get()
_handler = _getHandler()
return _delegate(_handler,module,name) return _delegate(_handler,module,name)
def _delegate(_handler,module,name): def _delegate(_handler,module,name):
@ -160,7 +176,7 @@ def _delegate(_handler,module,name):
_context = _handler.system()['context'] _context = _handler.system()['context']
if _context : if _context :
uri = f'{_context}/{uri}' uri = f'{_context}/{uri}'
_mimeType = 'application/octet-stream'
if uri not in _plugins : if uri not in _plugins :
_data = {} _data = {}
_code = 404 _code = 404
@ -171,19 +187,30 @@ def _delegate(_handler,module,name):
# else: # else:
# _data = pointer() # _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 : if type(_data) == pd.DataFrame :
_data = _data.to_dict(orient='records') _data = _data.to_dict(orient='records')
if type(_data) == list: if type(_data) == list:
_data = json.dumps(_data) _data = json.dumps(_data)
_code = 200 if _data else 500 _code = 200 if _data else 500
return _data,_code return _data,_code,{'Content-Type':_mimeType}
@_app.route("/api/<module>/<name>" , methods=['POST'])
def _post (module,name): # @_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 _config
# global _route # global _route
# _handler = _route.get() # _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) return _delegate(_handler,module,name)
@_app.route('/version') @_app.route('/version')
@ -192,25 +219,46 @@ def _version ():
_handler = _route.get() _handler = _route.get()
global _config global _config
return _handler.system()['version'] return _handler.system()['version']
@_app.route('/reload',methods=['POST']) @_app.route("/reload/<key>")
def reload(): def _reload(key) :
global _route global _route
_handler = _route.get_main() _handler = _route.get_main()
_system = _handler.system() _system = _handler.system()
_key = request.headers['key'] if 'key' in request.headers else None
if not 'source' in _system : if not 'source' in _system :
_systemKey = None _systemKey = None
elif 'key' in _system['source'] and _system['source']['key']: elif 'key' in _system['source'] and _system['source']['key']:
_systemKey = _system['source']['key'] _systemKey = _system['source']['key']
print ([_key,_systemKey,_systemKey == _key]) print ([key,_systemKey,_systemKey == key])
if _key and _systemKey and _systemKey == _key : if key and _systemKey and _systemKey == key :
_handler.reload() _handler.reload()
return "",200 return "",200
pass pass
return "",403 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 return the content of a folder formatted for a menu
""" """
@ -218,7 +266,9 @@ def cms_page():
global _route global _route
# _handler = _route.get() # _handler = _route.get()
# _config = _handler.config() # _config = _handler.config()
_handler = _getHandler() _handler = _getHandler(app_id,key)
_setHandler(app_id,key)
_config = _handler.config() _config = _handler.config()
# _uri = os.sep.join([_config['layout']['root'],request.headers['uri']]) # _uri = os.sep.join([_config['layout']['root'],request.headers['uri']])
_uri = request.headers['uri'] _uri = request.headers['uri']
@ -241,11 +291,13 @@ def cms_page():
if 'read?uri=' in _uri or 'download?doc=' in _uri : if 'read?uri=' in _uri or 'download?doc=' in _uri :
_uri = _uri.split('=')[1] _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 _args[_id],200
# return _html,200 # return _html,200
@_app.route('/page') @_app.route('/page',defaults={'app_id':'main','resource':None})
def _cms_page (): @_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 _config
global _route global _route
# _handler = _route.get() # _handler = _route.get()
@ -254,33 +306,33 @@ def _cms_page ():
# _uri = os.sep.join([_config['layout']['root'],_uri]) # _uri = os.sep.join([_config['layout']['root'],_uri])
_title = request.args['title'] if 'title' in request.args else '' _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 return _args[_title],200
@_app.route('/set/<id>') # @_app.route('/set/<id>')
def set(id): # def set(id):
global _route # global _route
_setHandler(id) # _setHandler(id)
# _route.set(id) # # _route.set(id)
# _handler = _route.get() # # _handler = _route.get()
_handler = _getHandler() # _handler = _getHandler()
_context = _handler.system()['context'] # _context = _handler.system()['context']
_uri = f'/{_context}'.replace('//','/') # _uri = f'/{_context}'.replace('//','/')
return redirect(_uri) # return redirect(_uri)
@_app.route('/<id>') # @_app.route('/<id>')
def _open(id): # def _open(id):
global _route # global _route
# _handler = _route.get() # # _handler = _route.get()
_handler = _getHandler() # _handler = _getHandler()
if id not in _route._apps : # if id not in _route._apps :
_args = {'config':_handler.config(), 'layout':_handler.layout(),'system':_handler.system(skip=['source','app'])} # _args = {'config':_handler.config(), 'layout':_handler.layout(),'system':_handler.system(skip=['source','app'])}
return render_template("404.html",**_args) # return render_template("404.html",**_args)
else: # else:
_setHandler(id) # _setHandler(id)
# _route.set(id) # # _route.set(id)
return _index() # return _index()
@cli.command() @cli.command()
@ -307,6 +359,7 @@ def start (
# _route = cms.engine.Router(**_args) #path=path,shared=shared) # _route = cms.engine.Router(**_args) #path=path,shared=shared)
_route = cms.engine.basic.CMS(**_args) _route = cms.engine.basic.CMS(**_args)
# dir(_route) # dir(_route)
# _args = _route.get().get_app() # _args = _route.get().get_app()
_args = _route.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 { .menu {
padding:8px; padding:8px;
border:1px solid #CAD5E0 ; border:1px solid #CAD5E0 ;
@ -44,4 +49,33 @@
display:block; display:block;
height:auto; 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 * components: search
*/ */
/* .search-box {
display:grid;
grid-template-columns: auto 64px; gap:4px;
} */
.search .frame .suggestion-frame { .search .frame .suggestion-frame {
width:98%; width:98%;
overflow: hidden; 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-left:10%;
margin-right:10%; margin-right:10%;
gap:4px; gap:4px;
} }
.main .header { height:64px; .main .header { height:64px;
display:grid; display:grid;

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

@ -12,43 +12,32 @@ bootup.CMSObserver = function(_sysId,_domId,_fileURI){
var http = HttpClient.instance() var http = HttpClient.instance()
http.setHeader('uri',_fileURI) http.setHeader('uri',_fileURI)
if (sessionStorage[_sysId] != null){ if (sessionStorage[_sysId] != null ){
var uri = sessionStorage[_sysId]+'/page' var uri = sessionStorage[_sysId]+'/page'
}else{ }else{
var uri = '/page' var uri = '/page'
} }
if (window.location.pathname != '/'){
uri = ([window.location.pathname,'page']).join('/')
}
try{ try{
// var _domElement = jx.dom.get.instance('div') // var _domElement = jx.dom.get.instance('div')
// _domElement.className = 'busy-loading' // _domElement.className = 'busy-loading'
// jx.dom.append(_domId, _domElement) // jx.dom.append(_domId, _domElement)
http.post(uri,function(x){ 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){ if (x.status == 200){
// jx.dom.set.value(_domId,x.responseText)
// var _domElement = jx.dom.get.instance('div')
// _domElement.innerHTML = x.responseText
setTimeout(function(){ setTimeout(function(){
// _domElement.innerHTML = x.responseText _content = $(x.responseText)
// _domElement.className = null var _id = $(_content).attr('id')
// $(_domElement).html(x.responseText) _pid = (['#',_domId,' #',_id]).join('')
if( $(_pid).length != 0){
$(_pid).remove()
}
$('#'+_domId).append(x.responseText) $('#'+_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) },1500)

@ -52,7 +52,7 @@ menu.apply = function (uri,id,pid,_context){
} }
menu.apply_link =function(_args){ menu.apply_link =function(_args,_context){
// //
// type: // type:
// redirect open new window // redirect open new window
@ -75,7 +75,7 @@ menu.apply_link =function(_args){
http.setHeader('uri',_args.uri) http.setHeader('uri',_args.uri)
http.setHeader('dom',(_args.title)?_args.title:'dialog') http.setHeader('dom',(_args.title)?_args.title:'dialog')
// http.setHeader('dom',_args.text) // http.setHeader('dom',_args.text)
http.get('/dialog',function(x){ http.get(_context+'/dialog',function(x){
jx.modal.show({html:x.responseText,id:'dialog'}) jx.modal.show({html:x.responseText,id:'dialog'})
console.log([$('.jxmodal')]) 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">
<div class="icon"> <img src="{{system.icon}}">
<img src="{{system.icon}}"> </div>
</div>
{% endif %} <div>
<div> <div class="title">{{layout.header.title}}</div>
<div class="title">{{layout.header.title}}</div> <div class="subtitle">{{layout.header.subtitle}}</div>
<div class="subtitle">{{layout.header.subtitle}}</div> </div>
</div>

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

@ -1,7 +1,7 @@
{%if system.portal %} {%if system.portal %}
<div class="icon active"> <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> <i class="fa-solid fa-chevron-left" style="color:darkgray; display:block"></i>
<img src="{{system.caller.icon}}" style="height:100%"/> <img src="{{system.caller.icon}}" style="height:100%"/>
</div> </div>
@ -11,33 +11,7 @@
<i class="fa-solid fa-home"></i> <i class="fa-solid fa-home"></i>
</div> </div>
{% endif %} {% 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> <script>
{%endif%} menu.tabs.init({{layout|tojson}},'{{system.context}}')
</div> </script>
{%endfor%}

@ -1,4 +1,15 @@
__author__ = "Steve L. Nyemba<steve@the-phi.com>" __author__ = "Steve L. Nyemba"
__version__= "2.0" __version__= "2.1.6"
__email__ = "steve@the-phi.com"
__license__=""" __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