diff --git a/cms/__init__.py b/cms/__init__.py index d621116..2300b33 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -7,7 +7,7 @@ from jinja2 import Environment, BaseLoader, FileSystemLoader import importlib import importlib.util import json - +from . import sites from . import apexchart class Plugin : @@ -132,13 +132,18 @@ class delegate: # # 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 : _uri = f'{_context}/{_uri}' - _plugins = _handler.plugins() + # print ([' ** ',_uri, _args['uri']]) + # _plugins = _handler.plugins() + _plugins = _handler.get('plugins') _code = 200 + if _uri in _plugins : - _config = _handler.config() #_args['config'] + # _config = _handler.config() #_args['config'] + _config = _handler.get(None) #_args['config'] _pointer = _plugins[_uri] @@ -159,6 +164,7 @@ class delegate: if type(_data) == pd.DataFrame : _data = _data.to_json(orient='records') elif type(_data) in [dict,list] : + _data = json.dumps(_data) pass else: diff --git a/cms/disk.py b/cms/disk.py index bd1b6ce..4d105b4 100644 --- a/cms/disk.py +++ b/cms/disk.py @@ -43,11 +43,11 @@ def build (_config, keep=[]): #(_path,_content): :path path of the files on disk :config configuration associated with the """ + _path = _config['layout']['root'] # if 'location' in _config['layout'] : # _path = _config['layout']['location'] _path = _realpath(_path,_config) - # print (_path) _items = folders(_path,_config) _subItems = [ content (os.sep.join([_path,_name]),_config)for _name in _items ] @@ -57,7 +57,6 @@ def build (_config, keep=[]): #(_path,_content): _index = _items.index(_name) if _name.startswith('_') or len(_subItems[_index]) == 0: continue - # print ([_name,_subItems[_index]]) if _name not in _r : _r[_name] = [] _r[_name] += _subItems[_index] @@ -68,9 +67,8 @@ def build (_config, keep=[]): #(_path,_content): def _realpath (uri,_config) : _layout = _config['layout'] - _uri = copy.copy(uri) - if 'location' in _layout : + if 'location' in _layout and _layout['location']: _uri = os.sep.join([_layout['location'],_uri]) return _uri @@ -119,7 +117,9 @@ def html(_uri,_config) : _context = str(_config['system']['context']) # if '/' in _context : # _context = _context.split('/')[-1] + _html = ( open(_path)).read() + _layout = _config['layout'] if 'location' in _layout : if not _config : diff --git a/cms/engine/__init__.py b/cms/engine/__init__.py index 9d03eac..ed826f4 100644 --- a/cms/engine/__init__.py +++ b/cms/engine/__init__.py @@ -9,7 +9,7 @@ # import importlib # import importlib.util from cms import disk, cloud -from . import basic +# from . import basic # class Loader : # """ diff --git a/cms/engine/basic.py b/cms/engine/basic.py deleted file mode 100644 index d7201e8..0000000 --- a/cms/engine/basic.py +++ /dev/null @@ -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'
',_html, '
']) - 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'] diff --git a/cms/index.py b/cms/index.py index de5870b..d5234f4 100644 --- a/cms/index.py +++ b/cms/index.py @@ -26,51 +26,43 @@ import datetime import requests from cms import disk, cloud, engine +import cms.sites _app = Flask(__name__) cli = typer.Typer() -@_app.route('/favicon.ico') -@_app.route('//favicon.ico') -def favicon(id): - global _route - # _system = _route.get ().system() - # _handler = _route.get() - _handler = _getHandler(id) - _system = _handler.system() - _logo =_system['icon'] #if 'icon' in _system else 'static/img/logo.svg' - _stream = requests.get(''.join([request.host_url,_logo])) - - return "_stream",200,{"Content-Type":"image/png"} #_handler.get(_logo),200,{"content-type":"image/png"} - + def _getHandler (app_id,resource=None) : - global _route + global _qcms _id = _getId(app_id,resource) - return _route._apps[_id] + return _qcms._apps[_id] def _getId(app_id,app_x): 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 def _setHandler (app_id,resource) : session['app_id'] = _getId(app_id,resource) -@_app.route("/robots.txt") -def robots_txt(): +@_app.route("/<_id>/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 menu options """ - global _route - _system = _route.get ().system() - - _info = [''' + global _qcms + _site = _qcms.get (_id) + _routes = _site.get('system.routes') + _context = _site.get('system.context') + _info = [f''' User-agent: * - Allow: / + Allow: /{_context} '''] - if 'routes' in _system : - for _key in _system['routes'] : - _uri = '/'.join(['',_key]) + if _routes : + for _key in _routes: + _uri = '/'.join(["",_context,_key]) _info.append(f''' User-agent: * Allow: {_uri} @@ -82,14 +74,14 @@ def _getIndex (app_id ,resource=None): _handler = _getHandler(app_id,resource) _layout = _handler.layout() _status_code = 200 - global _route + global _qcms _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) + _args = _qcms.render(_uri,'index',_id) except Exception as e: _status_code = 404 @@ -98,108 +90,81 @@ def _getIndex (app_id ,resource=None): return render_template(_index_page,**_args),_status_code @_app.route("/") 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.route("/",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/') -# def people(uid): -# """ -# This function will implement hardened links that can directly "do something" -# """ -# global _config -# return "0",200 +def _altIndex(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 + @_app.route('//dialog') -@_app.route('/dialog',defaults={'app':'main'}) -def _dialog (app): - # global _config - global _route - _handler = _route.get() - _uri = request.headers['uri'] - +@_app.route('/dialog',defaults={'app':None}) +def _getdialog(app): + global _qcms + _site = _qcms.get(app) + _uri = request.headers['uri'] _id = request.headers['dom'] # _html = ''.join(["
",str( e.render(**_args)),'
']) - _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 return render_template('dialog.html',**_args) #title=_id,html=_html) -@_app.route("/api//",defaults={'app':'main','key':None},methods=['GET','POST','DELETE','PUT']) +@_app.route("/api//",defaults={'app':None,'key':None},methods=['GET','POST','DELETE','PUT']) @_app.route("//api//",defaults={'key':None},methods=['GET','POST','DELETE','PUT']) @_app.route("///api//",methods=['GET','POST','DELETE','PUT']) - -def _delegate_call(app,key,module,name): - _handler = _getHandler(app,key) - # print (_handler.config()/) - uri = f'api/{module}/{name}' - # return Plugin.call(uri=uri,handler=_handler,request=request) +def _proxyCall(app,key,module,name): + global _qcms + + _id = _getId(app,key) + + _site = _qcms.get(_id) + _uri = f'api/{module}/{name}' _delegate = cms.delegate() - return _delegate(uri=uri,handler=_handler,request=request) - + return _delegate(uri=_uri, handler=_site, request=request) @_app.route('/version') def _version (): - global _route - _handler = _route.get() + global _qcms + _site = _qcms.get() global _config - return _handler.system()['version'] + return _site.get('system.version') @_app.route("/reload/") def _reload(key) : - global _route + global _qcms - _handler = _route.get_main() - _system = _handler.system() - if not 'source' in _system : - _systemKey = None - elif 'key' in _system['source'] and _system['source']['key']: - _systemKey = _system['source']['key'] - # print ([key,_systemKey,_systemKey == key]) - if key and _systemKey and _systemKey == key : - _handler.reload() - return "",200 - pass - return "",403 + _site = _qcms.get(None) + _systemKey = _site.get('system.source.key') + if _systemKey == key and _systemKey: + _site.reload() + return "",200 + else: + return "",403 @_app.route('/reload',methods=['POST']) def reload(): - # global _route - - # _handler = _route.get_main() - # _system = _handler.system() _key = request.headers['key'] if 'key' in request.headers else None return _reload(_key) - # if not 'source' in _system : - # _systemKey = None - # elif 'key' in _system['source'] and _system['source']['key']: - # _systemKey = _system['source']['key'] - # print ([_key,_systemKey,_systemKey == _key]) - # if _key and _systemKey and _systemKey == _key : - # _handler.reload() - # return "",200 - # pass - # return "",403 + -@_app.route('/page',methods=['POST'],defaults={'app_id':'main','key':None}) +@_app.route('/page',methods=['POST'],defaults={'app_id':None,'key':None}) @_app.route('//page',methods=['POST'],defaults={'key':None}) @_app.route('///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 _route - # _handler = _route.get() - # _config = _handler.config() - _handler = _getHandler(app_id,key) - _setHandler(app_id,key) - - _config = _handler.config() - # _uri = os.sep.join([_config['layout']['root'],request.headers['uri']]) + global _qcms + _id = _getId(app_id,key) + _site = _qcms.get(_id) _uri = request.headers['uri'] if 'dom' not in request.headers : _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 : _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 _html,200 -@_app.route('/page',defaults={'app_id':'main','resource':None}) + +@_app.route('/page',defaults={'app_id':None,'resource':None}) @_app.route('//page',defaults={'resource':None},methods=['GET']) @_app.route('///page',methods=['GET']) def _cms_page (app_id,resource): - # global _config - global _route - # _handler = _route.get() - # _config = _handler.config() + global _qcms _uri = request.args['uri'] - - # _uri = os.sep.join([_config['layout']['root'],_uri]) _title = request.args['title'] if 'title' in request.args else '' - _args = _route.render(_uri,_title,session.get(app_id,'main')) + _args = _qcms.render(_uri,_title,app_id) return _args[_title],200 @@ -238,23 +198,16 @@ def start ( :path path to the the manifest :shared run in shared mode i.e """ - global _route + global _qcms if os.path.exists(path) and os.path.isfile(path): _args = {'path':path} - # if shared : - # _args['location'] = path - # _args['shared'] = shared _args['location'] = path _args['shared'] = True - # _route = cms.engine.Router(**_args) #path=path,shared=shared) - - _route = cms.engine.basic.CMS(**_args) + _qcms = cms.sites.QCMS(**_args) + _args = _qcms.get().get('system.app') - # dir(_route) - # _args = _route.get().get_app() - _args = _route.get().app() if port : _args['port'] = port _app.secret_key = str(uuid.uuid4()) diff --git a/cms/sites/__init__.py b/cms/sites/__init__.py new file mode 100644 index 0000000..d0d8ca2 --- /dev/null +++ b/cms/sites/__init__.py @@ -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'
',_html,"
"]) + +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] diff --git a/cms/static/js/forms.js b/cms/static/js/forms.js new file mode 100644 index 0000000..f068d7a --- /dev/null +++ b/cms/static/js/forms.js @@ -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 = $('
').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 = $('
') + _label = $('
').html(label) + _input = $('').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 = $('
') + choices.forEach(_value=>{ + _option = $('
').addClass("option").addClass("option-"+_value) + $(_pane).append(_option) + }) + _pane.append(_input,choices) + _frame = $('
').append(_pane) + return _frame + } +} \ No newline at end of file diff --git a/cms/static/js/jx/utils.js b/cms/static/js/jx/utils.js index a3834ac..52d54a5 100755 --- a/cms/static/js/jx/utils.js +++ b/cms/static/js/jx/utils.js @@ -196,6 +196,7 @@ jx.utils.patterns.observer = function(lobservers,init){ // let's fire the design pattern // p.start() ; + return p } /** diff --git a/cms/static/js/menu.js b/cms/static/js/menu.js index 04a2f1b..7d234a2 100644 --- a/cms/static/js/menu.js +++ b/cms/static/js/menu.js @@ -281,7 +281,6 @@ var QCMSBasic= function(_layout,_context,_clickEvent) { // Object.keys(this._layout.menu) _names.forEach(function(_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 // @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._context = _context this._layout = _layout + this._target = (_id == null)?'.main .menu':_id this._make = function (text,_item,_event){ var text = text.trim().replace(/(_|-)/ig,' ').trim() var _context = this._context; @@ -364,9 +364,9 @@ var QCMSTabs = function(_layout,_context,_clickEvent){ }) this.tabs.className = 'tabs' - $('.main .menu').append(this.tabs) - $('.main .menu').css({'border':'1px solid transparent'}) - $('.main .menu').css({'grid-template-columns':'64px auto'}) + $(this._target).append(this.tabs) + $(this._target).css({'border':'1px solid transparent'}) + $(this._target).css({'grid-template-columns':'64px auto'}) } // // We need to load the pages here ... diff --git a/cms/templates/index.html b/cms/templates/index.html index 347d440..c08f957 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -15,6 +15,7 @@ Vanderbilt University Medical Center + {{layout.header.title}} @@ -37,6 +38,7 @@ Vanderbilt University Medical Center + @@ -44,6 +46,7 @@ Vanderbilt University Medical Center + @@ -84,8 +87,8 @@ Vanderbilt University Medical Center -
-