Merge pull request 'integration of branch - v2.2' (#25) from v2.2 into master

Reviewed-on: cloud/cms#25
master
Steve L. Nyemba 3 weeks ago
commit 415e3401a5

@ -7,7 +7,7 @@ from jinja2 import Environment, BaseLoader, FileSystemLoader
import importlib import importlib
import importlib.util import importlib.util
import json import json
from . import sites
from . import apexchart from . import apexchart
class Plugin : class Plugin :
@ -132,13 +132,18 @@ class delegate:
# #
# we need to update the _uri (if context/embeded apps) # we need to update the _uri (if context/embeded apps)
# #
_context = _handler.system()['context'] # _context = _handler.system()['context']
_context = _handler.get('system.context')
if _context : if _context :
_uri = f'{_context}/{_uri}' _uri = f'{_context}/{_uri}'
_plugins = _handler.plugins() # print ([' ** ',_uri, _args['uri']])
# _plugins = _handler.plugins()
_plugins = _handler.get('plugins')
_code = 200 _code = 200
if _uri in _plugins : if _uri in _plugins :
_config = _handler.config() #_args['config'] # _config = _handler.config() #_args['config']
_config = _handler.get(None) #_args['config']
_pointer = _plugins[_uri] _pointer = _plugins[_uri]
@ -159,6 +164,7 @@ class delegate:
if type(_data) == pd.DataFrame : if type(_data) == pd.DataFrame :
_data = _data.to_json(orient='records') _data = _data.to_json(orient='records')
elif type(_data) in [dict,list] : elif type(_data) in [dict,list] :
_data = json.dumps(_data) _data = json.dumps(_data)
pass pass
else: else:

@ -43,11 +43,11 @@ def build (_config, keep=[]): #(_path,_content):
:path path of the files on disk :path path of the files on disk
:config configuration associated with the :config configuration associated with the
""" """
_path = _config['layout']['root'] _path = _config['layout']['root']
# if 'location' in _config['layout'] : # if 'location' in _config['layout'] :
# _path = _config['layout']['location'] # _path = _config['layout']['location']
_path = _realpath(_path,_config) _path = _realpath(_path,_config)
# 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 ]
@ -57,7 +57,6 @@ def build (_config, keep=[]): #(_path,_content):
_index = _items.index(_name) _index = _items.index(_name)
if _name.startswith('_') or len(_subItems[_index]) == 0: if _name.startswith('_') or len(_subItems[_index]) == 0:
continue continue
# print ([_name,_subItems[_index]])
if _name not in _r : if _name not in _r :
_r[_name] = [] _r[_name] = []
_r[_name] += _subItems[_index] _r[_name] += _subItems[_index]
@ -68,9 +67,8 @@ def build (_config, keep=[]): #(_path,_content):
def _realpath (uri,_config) : def _realpath (uri,_config) :
_layout = _config['layout'] _layout = _config['layout']
_uri = copy.copy(uri) _uri = copy.copy(uri)
if 'location' in _layout : if 'location' in _layout and _layout['location']:
_uri = os.sep.join([_layout['location'],_uri]) _uri = os.sep.join([_layout['location'],_uri])
return _uri return _uri
@ -119,7 +117,9 @@ def html(_uri,_config) :
_context = str(_config['system']['context']) _context = str(_config['system']['context'])
# if '/' in _context : # if '/' in _context :
# _context = _context.split('/')[-1] # _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 :
if not _config : if not _config :

@ -9,7 +9,7 @@
# 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 :
# """ # """

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

@ -26,51 +26,43 @@ import datetime
import requests import requests
from cms import disk, cloud, engine from cms import disk, cloud, engine
import cms.sites
_app = Flask(__name__) _app = Flask(__name__)
cli = typer.Typer() cli = typer.Typer()
@_app.route('/favicon.ico')
@_app.route('/<id>/favicon.ico')
def favicon(id):
global _route
# _system = _route.get ().system()
# _handler = _route.get()
_handler = _getHandler(id)
_system = _handler.system()
_logo =_system['icon'] #if 'icon' in _system else 'static/img/logo.svg'
_stream = requests.get(''.join([request.host_url,_logo]))
return "_stream",200,{"Content-Type":"image/png"} #_handler.get(_logo),200,{"content-type":"image/png"}
def _getHandler (app_id,resource=None) : def _getHandler (app_id,resource=None) :
global _route global _qcms
_id = _getId(app_id,resource) _id = _getId(app_id,resource)
return _route._apps[_id] return _qcms._apps[_id]
def _getId(app_id,app_x): def _getId(app_id,app_x):
if app_x not in [None,''] : if app_x not in [None,''] :
return '/'.join([app_id,app_x]) _uri = '/'.join([app_id,app_x])
return _uri[:-1] if _uri.endswith('/') else _uri
return app_id return app_id
def _setHandler (app_id,resource) : def _setHandler (app_id,resource) :
session['app_id'] = _getId(app_id,resource) session['app_id'] = _getId(app_id,resource)
@_app.route("/robots.txt") @_app.route("/<_id>/robots.txt")
def robots_txt(): @_app.route("/robots.txt",defaults={'_id':None})
def robots_txt(_id):
""" """
This function will generate a robots expression for a variety of crawlers, the paths will be provided by This function will generate a robots expression for a variety of crawlers, the paths will be provided by
menu options menu options
""" """
global _route global _qcms
_system = _route.get ().system() _site = _qcms.get (_id)
_routes = _site.get('system.routes')
_info = [''' _context = _site.get('system.context')
_info = [f'''
User-agent: * User-agent: *
Allow: / Allow: /{_context}
'''] ''']
if 'routes' in _system : if _routes :
for _key in _system['routes'] : for _key in _routes:
_uri = '/'.join(['',_key]) _uri = '/'.join(["",_context,_key])
_info.append(f''' _info.append(f'''
User-agent: * User-agent: *
Allow: {_uri} Allow: {_uri}
@ -82,14 +74,14 @@ def _getIndex (app_id ,resource=None):
_handler = _getHandler(app_id,resource) _handler = _getHandler(app_id,resource)
_layout = _handler.layout() _layout = _handler.layout()
_status_code = 200 _status_code = 200
global _route global _qcms
_index_page = 'index.html' _index_page = 'index.html'
_args = {} _args = {}
try : try :
_uri = os.sep.join([_layout['root'],_layout['index']]) _uri = os.sep.join([_layout['root'],_layout['index']])
_id = _getId(app_id,resource) _id = _getId(app_id,resource)
_args = _route.render(_uri,'index',_id) _args = _qcms.render(_uri,'index',_id)
except Exception as e: except Exception as e:
_status_code = 404 _status_code = 404
@ -98,108 +90,81 @@ def _getIndex (app_id ,resource=None):
return render_template(_index_page,**_args),_status_code return render_template(_index_page,**_args),_status_code
@_app.route("/") @_app.route("/")
def _index (): def _index ():
return _getIndex('main') # return _getIndex('main')
global _qcms
_app = _qcms.get(None)
_uri = os.sep.join([_app.get('layout.root'),_app.get('layout.index')])
_html = _qcms.render(_uri,'index')
return render_template('index.html',**_qcms.render(_uri,'index')),200
@_app.route("/<app>/<resource>") @_app.route("/<app>/<resource>")
@_app.route("/<app>",defaults={'resource':None}) @_app.route("/<app>",defaults={'resource':None})
def _aindex (app,resource=None): def _altIndex(app,resource):
_handler = _getHandler(app,resource) global _qcms
_id = _getId(app,resource)
_site = _qcms.get(_id)
_uri = os.sep.join([_site.get('layout.root'),_site.get('layout.index')])
return render_template('index.html',**_qcms.render(_uri,'index',_id)),200
_setHandler(app,resource)
_html,_code = _getIndex(app,resource)
return _html,_code
# @_app.route('/id/<uid>')
# def people(uid):
# """
# This function will implement hardened links that can directly "do something"
# """
# global _config
# return "0",200
@_app.route('/<app>/dialog') @_app.route('/<app>/dialog')
@_app.route('/dialog',defaults={'app':'main'}) @_app.route('/dialog',defaults={'app':None})
def _dialog (app): def _getdialog(app):
# global _config global _qcms
global _route _site = _qcms.get(app)
_handler = _route.get()
_uri = request.headers['uri'] _uri = request.headers['uri']
_id = request.headers['dom'] _id = request.headers['dom']
# _html = ''.join(["<div style='padding:1%'>",str( e.render(**_args)),'</div>']) # _html = ''.join(["<div style='padding:1%'>",str( e.render(**_args)),'</div>'])
_args = _route.render(_uri,'html',app) #session.get('app_id','main')) _args = _qcms.render(_uri,'html',app) #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("/api/<module>/<name>",defaults={'app':'main','key':None},methods=['GET','POST','DELETE','PUT']) @_app.route("/api/<module>/<name>",defaults={'app':None,'key':None},methods=['GET','POST','DELETE','PUT'])
@_app.route("/<app>/api/<module>/<name>",defaults={'key':None},methods=['GET','POST','DELETE','PUT']) @_app.route("/<app>/api/<module>/<name>",defaults={'key':None},methods=['GET','POST','DELETE','PUT'])
@_app.route("/<app>/<key>/api/<module>/<name>",methods=['GET','POST','DELETE','PUT']) @_app.route("/<app>/<key>/api/<module>/<name>",methods=['GET','POST','DELETE','PUT'])
def _proxyCall(app,key,module,name):
global _qcms
def _delegate_call(app,key,module,name): _id = _getId(app,key)
_handler = _getHandler(app,key)
# print (_handler.config()/)
uri = f'api/{module}/{name}'
# return Plugin.call(uri=uri,handler=_handler,request=request)
_delegate = cms.delegate()
return _delegate(uri=uri,handler=_handler,request=request)
_site = _qcms.get(_id)
_uri = f'api/{module}/{name}'
_delegate = cms.delegate()
return _delegate(uri=_uri, handler=_site, request=request)
@_app.route('/version') @_app.route('/version')
def _version (): def _version ():
global _route global _qcms
_handler = _route.get() _site = _qcms.get()
global _config global _config
return _handler.system()['version'] return _site.get('system.version')
@_app.route("/reload/<key>") @_app.route("/reload/<key>")
def _reload(key) : def _reload(key) :
global _route global _qcms
_handler = _route.get_main() _site = _qcms.get(None)
_system = _handler.system() _systemKey = _site.get('system.source.key')
if not 'source' in _system : if _systemKey == key and _systemKey:
_systemKey = None _site.reload()
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 return "",200
pass else:
return "",403 return "",403
@_app.route('/reload',methods=['POST']) @_app.route('/reload',methods=['POST'])
def reload(): def reload():
# global _route
# _handler = _route.get_main()
# _system = _handler.system()
_key = request.headers['key'] if 'key' in request.headers else None _key = request.headers['key'] if 'key' in request.headers else None
return _reload(_key) 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('/page',methods=['POST'],defaults={'app_id':None,'key':None})
@_app.route('/<app_id>/page',methods=['POST'],defaults={'key':None}) @_app.route('/<app_id>/page',methods=['POST'],defaults={'key':None})
@_app.route('/<app_id>/<key>/page',methods=['POST']) @_app.route('/<app_id>/<key>/page',methods=['POST'])
def _POST_CMSPage(app_id,key): def getPostedPage(app_id,key):
""" """
return the content of a folder formatted for a menu This function will return a page given a location parameter
""" """
# global _config global _qcms
global _route _id = _getId(app_id,key)
# _handler = _route.get() _site = _qcms.get(_id)
# _config = _handler.config()
_handler = _getHandler(app_id,key)
_setHandler(app_id,key)
_config = _handler.config()
# _uri = os.sep.join([_config['layout']['root'],request.headers['uri']])
_uri = request.headers['uri'] _uri = request.headers['uri']
if 'dom' not in request.headers : if 'dom' not in request.headers :
_id = _uri.split('/')[-1].split('.')[0] _id = _uri.split('/')[-1].split('.')[0]
@ -208,22 +173,17 @@ def _POST_CMSPage(app_id,key):
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,_getId(app_id,key)) #session.get(app_id,'main')) _args = _qcms.render(_uri,_id,_getId(app_id,key)) #session.get(app_id,'main'))
return _args[_id],200 return _args[_id],200
# return _html,200
@_app.route('/page',defaults={'app_id':'main','resource':None}) @_app.route('/page',defaults={'app_id':None,'resource':None})
@_app.route('/<app_id>/page',defaults={'resource':None},methods=['GET']) @_app.route('/<app_id>/page',defaults={'resource':None},methods=['GET'])
@_app.route('/<app_id>/<resource>/page',methods=['GET']) @_app.route('/<app_id>/<resource>/page',methods=['GET'])
def _cms_page (app_id,resource): def _cms_page (app_id,resource):
# global _config global _qcms
global _route
# _handler = _route.get()
# _config = _handler.config()
_uri = request.args['uri'] _uri = request.args['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 = _qcms.render(_uri,_title,app_id)
return _args[_title],200 return _args[_title],200
@ -238,23 +198,16 @@ def start (
:path path to the the manifest :path path to the the manifest
:shared run in shared mode i.e :shared run in shared mode i.e
""" """
global _route global _qcms
if os.path.exists(path) and os.path.isfile(path): if os.path.exists(path) and os.path.isfile(path):
_args = {'path':path} _args = {'path':path}
# if shared :
# _args['location'] = path
# _args['shared'] = shared
_args['location'] = path _args['location'] = path
_args['shared'] = True _args['shared'] = True
# _route = cms.engine.Router(**_args) #path=path,shared=shared) _qcms = cms.sites.QCMS(**_args)
_args = _qcms.get().get('system.app')
_route = cms.engine.basic.CMS(**_args)
# dir(_route)
# _args = _route.get().get_app()
_args = _route.get().app()
if port : if port :
_args['port'] = port _args['port'] = port
_app.secret_key = str(uuid.uuid4()) _app.secret_key = str(uuid.uuid4())

@ -0,0 +1,352 @@
"""
This file will describe how a site is loaded
"""
from cms.engine.config.structure import Layout, System
from cms import disk, cloud
import copy
import json
import io
import os
from jinja2 import Environment, BaseLoader, FileSystemLoader
from datetime import datetime
class IOConfig:
"""
This class encapsulates read/write for configuration
"""
def __init__(self,**_args):
self._config = {'system':{},'layout':{},'plugins':{}}
self._caller = None
self._location= _args['location'] if 'location' in _args else None
self._logs = []
def get(self,_key) :
if not _key :
return self._config
_keys = _key.split('.') if '.' in _key else [_key]
_value= None
_object = copy.deepcopy(self._config)
for _key in _keys :
if _key in _object :
_object = _object[_key]
else:
return None
return _object
def set(self,_expr,value):
if len(_expr.split('.')) == 1 :
self._config[_expr] = value
else:
_object = self._config
for _attr in _expr.split('.')[:-1] :
if _attr not in _object :
_object[_attr] = {}
_object = _object[_attr]
_attr = _expr.split('.')[-1]
_object[_attr] = value
def log(self,**_args):
_row = dict({'_date':datetime.now().strftime('%Y-%m-%d'),'time':datetime.now().strftime('%H:%M:%S'), 'context':self.get('system.context')},**_args)
self._logs.append(_row)
def debug(self,**_args):
return self._logs,'application/json'
class Initialization (IOConfig):
def __init__(self,**_args):
super().__init__(**_args)
self._config = self.read_config(**_args)
self._args = _args
#
# Invoke initialization
self.reload()
def reload (self):
_args = self._args
self._caller = _args['caller'] if 'caller' in _args else None
if (self._caller):
self._config['system']['parentContext'] = ''
if not self.get('system.icon') :
self._config['system']['icon'] = None
self._config['layout']['location'] = None
if not self.get('layout.menu') :
self._config['layout']['menu'] = {}
self.context(**_args)
self.menu()
self.plugins()
def read_config(self,**_args) :
_config = {}
if 'path' in _args :
f = open(_args['path'])
_config = json.loads(f.read())
f.close()
elif 'stream' in _args :
_stream = _args['stream']
if type(_stream) == 'str' :
_config = json.loads(_stream)
elif type(_stream) == io.StringIO :
_config = json.loads( _stream.read())
_name = self._caller.__name__ if self._caller else None
self.log(action='init.config',module='read_config',input={'caller':_name})
return _config
def context(self,**_args):
"""
Updating the context so that we can have a consistent representation
"""
if ('context' in _args):
self.set('system.context',_args['context'])
pass
_context = self.get('system.context')
if self._caller :
#
# There is a parent context we need to account and we are updating the current context to reflect the caller context
_parentContext = self._caller.get('system.context')
_context = '/'.join([self._caller.get('system.context'),_context])
#
# updating the configuratioin
_iconURI = '/'.join(["",_parentContext,self._caller.get('system.icon')])
# print ([self._caller.get('system.context'), _parentContext,_context])
if self._caller.get('system.context') != '':
_parentContext = "/"+_parentContext
_context = "/"+_context
self.set('system.onport',0)
self._config['system']['onport'] = 0
else:
_iconURI = _iconURI.replace("///","/")
self.set('system.onport',1)
self._config['system']['onport'] = 1
self.set('system.caller',{'icon':_iconURI,'context':self._caller.get('system.context')})
# _context = _context.replace('/','')
else:
#
# If we have no parent site, there should be no parent context (or empty string)
_parentContext = _context #@TODO: should be none ?
#
# Updating context so it is usable throughout the code base
if self._caller:
self.set('system.parentContext', _parentContext.strip())
self.set('system.context',_context.strip())
# self._config['system']['context'] = _context.strip()
self._config['system']['parentContext'] = _parentContext
p = {'has_caller':self._caller != None,'context':_context}
self.log(action='init.context',module='context',input=p)
#
# loosly context to a certain extent involves locations of icons and document root
#
self.locations(**_args)
#
# submit log of status
def locations(self,**_args):
"""
We are updating the icons, project location file and security key (if any), we need to make sure that context is updated first
"""
#
# updating location & path of the application
# if self.get('system.caller') :
# return
_context = self.get('system.context')
_logo = self.get('system.logo')
_root = self.get('layout.root')
if self.get('system.source.id') == 'cloud' :
_icon = f'{_context}/api/cloud/download?doc={_logo}'
else:
_icon = f'{_context}/api/disk/read?uri={_logo}'
# _root = f'{_context}/api/disk/read?uri={_root}'
# self.set('layout.root',_root)
self.set('system.icon',_icon)
self.set('system.logo',_icon)
#
# from the path provided we can determine the location of the project
_homefolder = os.sep.join(_args['path'].split(os.sep)[:-1])
if _homefolder and os.path.exists(os.sep.join([_homefolder,_root])) :
self.set('layout.location',_homefolder)
#
# updating the security key and trying to determine the location
if self.get('system.source.key') :
_path = self.get('system.source.key')
f = None
if os.path.exists(_path) :
f = open(_path)
elif os.path.exists(os.sep.join([self.get('layout.location'),_path])) :
f = open(os.sep.join([self.get('layout.location'),_path]))
if f :
self.set('system.source.key',f.read())
f.close()
def menu (self,**_args):
"""
This function will build the menu of the site (if need be)
"""
_handler = cloud if self.get('system.source.id') == 'cloud' else disk
_object = _handler.build(self._config)
_overwrite = self.get('layout.overwrite')
for _name in _object :
_submenu = _object[_name]
_index = 0
for _item in _submenu :
_text = _item['text'].strip()
if _text in _overwrite :
if 'uri' in _item and 'url' in _overwrite :
del _item['uri']
_item = dict(_item,**_overwrite[_text])
_submenu[_index] = _item
_index += 1
self.set('layout.menu',_object)
# self._config['layout']['menu'] = _object
self.routes() #-- other apps/sites integrated in case we are operating in portal mode
#
# At this point the entire menu is build and we need to have it sorted
self.order()
# _labels = list(self.get('layout.menu').keys())
# self.log(action='init.menu',module='menu',input=_labels)
# print (self.get('layout.menu'))
# print (_object)
def order(self,**_args):
if self.get('layout.order.menu') :
_sorted = {}
_menu = self.get('layout.menu')
_ordered = self.get('layout.order.menu')
for _name in _ordered :
if _name in _menu :
_sorted[_name] = _menu[_name]
if _sorted :
_menu = _sorted
_missing = list(set(_menu.keys()) - set(_sorted))
if _missing :
for _name in _missing :
_menu[_name] = _menu[_name]
self.set('layout.menu',_menu)
def routes (self,**_args):
"""
This method will update menu with dependent sites/apps and add them to the menu
"""
_overwrite = self.get('layout.overwrite')
_routes = self.get('system.routes')
_context = self.get('system.context')
_menu = self.get('layout.menu')
if _routes :
for _text in _routes :
_item = _routes[_text]
if 'menu' in _item :
_uri = f'{_context}/{_text}'
_label = _item['menu']
if _text in _overwrite :
_text = _overwrite[_text]['text']
if _label not in _menu :
_menu[_label] = []
_menu[_label].append({"text":_text,"uri":_uri,"type":"open"})
#
# updating menu ...
self.set('layout.menu',_menu)
# self._config['layout']['menu'] = _menu
else:
pass
def plugins (self,**_args):
"""
This function will load the plugins from disk (only)
"""
_folder = os.sep.join([self.get('layout.location'),self.get('layout.root'),'_plugins'])
_context = self.get('system.context')
_parentContext = self.get('system.parentContext')
_map = {}
_plugins = {}
if self.get('system.source.id') == 'cloud' :
_plugins = cloud.plugins(_context)
else:
_plugins = disk.plugins(context=_context)
_uri = 'api/system/debug'
_uri = _uri if not _context else f'{_context}/{_uri}'
_plugins[_uri] = self.debug
if os.path.exists(_folder) and self.get('plugins'):
_items = self.get('plugins')
for _filename in _items:
_path = os.sep.join([_folder,_filename+'.py'])
if os.path.exists(_path) :
for _module in _items[_filename] :
_pointer = disk.plugins(path=_path,name=_module,context=_context)
if _pointer :
_uri = f"api/{_filename}/{_module}"
_uri = f"{_context}/{_uri}" if _context else _uri
_map[_uri] = _pointer
if _parentContext :
# _uri = f"{_parentContext}/{_context}"
_map[_uri] = _pointer
self.log(action='load.plugin',module='plugins',input={'uri':_uri,'path':_path,'name':_module})
else:
self.log(action='missing.function',module='plugins',input={'name':_module,'path':_path})
else:
#
# log the error ...
self.log(action='missing.plugins',module='plugins',input=_path)
#
# Updating plugins from disk/cloud
_plugins = _map if not _plugins else dict(_plugins,**_map)
#
#
self.set('plugins',_plugins)
self.log(action='init.plugins',module='plugins',input=list(_plugins.keys()))
class Site(Initialization) :
def __init__(self,**_args):
super().__init__(**_args)
self._config['system']['portal'] = (self.get('system.routes')) == None
def html(self,_uri,_id) :
_handler = cloud if self.get('system.source.id') == 'cloud' else disk
_html = _handler.html(_uri, self.get(None))
return " ".join([f'<div id="{_id}"> ',_html,"</div>"])
class QCMS:
def __init__(self,**_args):
_app = Site(**_args)
self._id = _app.get('system.context') #if _app.get('system.context') else 'main'
self._sites = {self._id:_app}
if _app.get('system.routes') :
_routes = _app.get('system.routes')
for _name in _routes :
_path = _routes[_name]['path']
self._sites[_name] = Site(context=_name,path=_path,caller=_app)
def render(self,_uri,_id,_appid=None):
_site = self._sites[_appid] if _appid else self._sites[self._id]
_args = {'layout':_site.get('layout')}
_system = _site.get('system')
for k in ['source','app'] :
if k in _system :
del _system[k]
_args['system'] = _system
_html = _site.html(_uri,_id)
_env = Environment(loader=BaseLoader()).from_string(_html)
_args[_id] = str(_env.render(**_args))
return _args
def set(self,_id):
self._id = _id
def get(self,_id=None):
return self._sites[self._id] if not _id else self._sites[_id]

@ -0,0 +1,60 @@
/***
*
* This file build forms from a given set of specifications (in JSON) format
*/
var _form =function(_id,_data) {
this._data = _data // {input,defaults,choices}
this._id = _id
//
// let's clean up the data :
this._data.defaults = (this._data.defaults == null)? {}:this._data.defaults
this._data.choices = (this._data.choices == null)?{}: this._data.choices
this.render = function (){
_title = $('<div></div>').html(this._id).addClass(this._id)
var _input = this._data.input
var _defaults = this._data.defaults
var _choices = this._data.choices
_input.forEach (_label=>{
_value = _defaults[_label]
if (_choices[_label]){
}else{
//
// This
_input = this.get.input(_label,_value)
}
})
}
this.get = {}
this.get.input = function (label,value){
_frame = $('<div></div>')
_label = $('<div></div>').html(label)
_input = $('<input type="text"/>').attr('placeholder',label.0.replace(/_/,' ').trim()).addClass("input-"+label)
$(_input)[0].data = label
if (value){
$(_input).val(value)
}
_frame.append(_label,_input)
return _frame
}
this.get.choice= function (label,_default,choices) {
var _input = this.get.input(label,_default)
$(input).addClass("hidden-input")
$(_input).on('click',function (){
})
_pane = $('<div class="choices"></div>')
choices.forEach(_value=>{
_option = $('<div><div>').addClass("option").addClass("option-"+_value)
$(_pane).append(_option)
})
_pane.append(_input,choices)
_frame = $('<div></div>').append(_pane)
return _frame
}
}

@ -196,6 +196,7 @@ jx.utils.patterns.observer = function(lobservers,init){
// let's fire the design pattern // let's fire the design pattern
// //
p.start() ; p.start() ;
return p
} }
/** /**

@ -281,7 +281,6 @@ var QCMSBasic= function(_layout,_context,_clickEvent) {
// Object.keys(this._layout.menu) // Object.keys(this._layout.menu)
_names.forEach(function(_name){ _names.forEach(function(_name){
var _div = _me._make(_layout.menu[_name]) ; var _div = _me._make(_layout.menu[_name]) ;
@ -299,7 +298,7 @@ var QCMSBasic= function(_layout,_context,_clickEvent) {
} }
} }
var QCMSTabs = function(_layout,_context,_clickEvent){ var QCMSTabs = function(_layout,_context,_id){
// //
// This object will make tabs in the area of the menu // This object will make tabs in the area of the menu
// @TODO: we can parameterize where the menu is made to improve flexibility // @TODO: we can parameterize where the menu is made to improve flexibility
@ -308,6 +307,7 @@ var QCMSTabs = function(_layout,_context,_clickEvent){
this.tabs.className = 'tabs' this.tabs.className = 'tabs'
this._context = _context this._context = _context
this._layout = _layout this._layout = _layout
this._target = (_id == null)?'.main .menu':_id
this._make = function (text,_item,_event){ this._make = function (text,_item,_event){
var text = text.trim().replace(/(_|-)/ig,' ').trim() var text = text.trim().replace(/(_|-)/ig,' ').trim()
var _context = this._context; var _context = this._context;
@ -364,9 +364,9 @@ var QCMSTabs = function(_layout,_context,_clickEvent){
}) })
this.tabs.className = 'tabs' this.tabs.className = 'tabs'
$('.main .menu').append(this.tabs) $(this._target).append(this.tabs)
$('.main .menu').css({'border':'1px solid transparent'}) $(this._target).css({'border':'1px solid transparent'})
$('.main .menu').css({'grid-template-columns':'64px auto'}) $(this._target).css({'grid-template-columns':'64px auto'})
} }
// //
// We need to load the pages here ... // We need to load the pages here ...

@ -15,6 +15,7 @@ Vanderbilt University Medical Center
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head > <head >
<title>{{layout.header.title}}</title> <title>{{layout.header.title}}</title>
<link rel="shortcut icon" href="{{system.icon}}"> <link rel="shortcut icon" href="{{system.icon}}">
@ -37,6 +38,7 @@ Vanderbilt University Medical Center
<link href="{{system.parentContext}}/static/css/dialog.css" rel="stylesheet" type="text/css"> <link href="{{system.parentContext}}/static/css/dialog.css" rel="stylesheet" type="text/css">
<link href="{{system.parentContext}}/static/css/dashboard.css" rel="stylesheet" type="text/css"> <link href="{{system.parentContext}}/static/css/dashboard.css" rel="stylesheet" type="text/css">
<!-- applying themes as can --> <!-- applying themes as can -->
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/layout.css" rel="stylesheet" type="text/css"> <link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/layout.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/header.css" rel="stylesheet" type="text/css"> <link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/header.css" rel="stylesheet" type="text/css">
@ -44,6 +46,7 @@ Vanderbilt University Medical Center
<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}}/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}}/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"> <link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/pane.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/responsive.css" rel="stylesheet" type="text/css">
<!-- --> <!-- -->
<meta property="og:title" content="{{layout.header.title}}" /> <meta property="og:title" content="{{layout.header.title}}" />
@ -85,7 +88,7 @@ Vanderbilt University Medical Center
</script> </script>
<body> <body>
<div class="main {{system.theme}}" > <div class="main {{system.theme}}" >
<div id="header" class="header" onclick="window.location.href='{{system.parentContext}}'" style="cursor:pointer"> <div id="header" class="header" onclick="window.location.href='{{system.context}}'" style="cursor:pointer">
{%include "header.html" %} {%include "header.html" %}
</div> </div>

@ -1,8 +1,12 @@
{%if system.portal %} {%if system.caller %}
{% if system.parentContext == ""%}
{% set _backURI = "/" %}
{% else %}
{% set _backURI = system.parentContext%}
{% endif %}
<div class="icon active"> <div class="icon active">
<div align="center" class="button" onclick="window.open('{{system.parentContext}}/','_self')" style="display:grid; grid-template-columns:auto auto; gap:4px; align-items:center "> <div align="center" class="button" onclick="window.open('{{_backURI}}','_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>
@ -12,3 +16,4 @@
<i class="fa-solid fa-home"></i> <i class="fa-solid fa-home"></i>
</div> </div>
{% endif %} {% endif %}

@ -38,6 +38,7 @@ The content is created in ./qcms-demo/www/html
Things to update with qcms runner : Things to update with qcms runner :
- system.icon location, when creating a project - system.icon location, when creating a project
- applying themes - applying themes
- upgrade all plugins to return data-type ? - upgrade all plugins to return data-type ?

Loading…
Cancel
Save