mirror of http://localhost:9400/cloud/cms
Merge pull request 'v2.1 - menu, themes, runner' (#10) from v2.1 into master
Reviewed-on: cloud/cms#10master
commit
bde8d235b3
@ -1,384 +1,385 @@
|
||||
import json
|
||||
|
||||
from genericpath import isdir
|
||||
import os
|
||||
import pandas as pd
|
||||
import transport
|
||||
import copy
|
||||
from jinja2 import Environment, BaseLoader, FileSystemLoader
|
||||
import importlib
|
||||
import importlib.util
|
||||
# import json
|
||||
|
||||
# from genericpath import isdir
|
||||
# import os
|
||||
# import pandas as pd
|
||||
# import transport
|
||||
# import copy
|
||||
# from jinja2 import Environment, BaseLoader, FileSystemLoader
|
||||
# import importlib
|
||||
# import importlib.util
|
||||
from cms import disk, cloud
|
||||
from . import basic
|
||||
|
||||
class Loader :
|
||||
"""
|
||||
This class is designed to exclusively load configuration from disk into an object
|
||||
:path path to the configuraiton file
|
||||
:location original location (caller)
|
||||
"""
|
||||
def __init__(self,**_args):
|
||||
self._path = _args['path']
|
||||
self._original_location = None if 'location' not in _args else _args['location']
|
||||
self._location = None
|
||||
self._caller = None if 'caller' not in _args else _args['caller']
|
||||
self._menu = {}
|
||||
self._plugins={}
|
||||
self.load()
|
||||
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
This function will load menu (overwrite) and plugins
|
||||
"""
|
||||
self.init_config()
|
||||
self.init_menu()
|
||||
self.init_plugins()
|
||||
def init_config(self):
|
||||
"""
|
||||
Initialize & loading configuration from disk
|
||||
"""
|
||||
f = open (self._path)
|
||||
self._config = json.loads(f.read())
|
||||
|
||||
if self._caller :
|
||||
self._location = self._original_location.split(os.sep) # needed for plugin loading
|
||||
self._location = os.sep.join(self._location[:-1])
|
||||
self._config['system']['portal'] = self._caller != None
|
||||
|
||||
#
|
||||
# let's see if we have a location for a key (i.e security key) in the configuration
|
||||
#
|
||||
self.update_config()
|
||||
# _system = self._config['system']
|
||||
# if 'source' in _system and 'key' in _system['source'] :
|
||||
# _path = _system['source']['key']
|
||||
# if os.path.exists(_path):
|
||||
# f = open(_path)
|
||||
# _system['source']['key'] = f.read()
|
||||
# f.close()
|
||||
# self._system = _system
|
||||
# self._config['system'] = _system
|
||||
def update_config(self):
|
||||
"""
|
||||
We are going to update the configuration (source.key, layout.root)
|
||||
"""
|
||||
_system = self._config['system']
|
||||
#
|
||||
# updating security-key that allows the application to update on-demand
|
||||
if 'source' in _system and 'key' in _system['source'] :
|
||||
_path = _system['source']['key']
|
||||
if os.path.exists(_path):
|
||||
f = open(_path)
|
||||
_system['source']['key'] = f.read()
|
||||
f.close()
|
||||
self._system = _system
|
||||
self._config['system'] = _system
|
||||
_layout = self._config['layout']
|
||||
#
|
||||
# update root so that the app can be launched from anywhere
|
||||
# This would help reduce the footprint of the app/framework
|
||||
_path = os.sep.join(self._path.split(os.sep)[:-1])
|
||||
_p = 'source' not in _system
|
||||
_q = 'source' in _system and _system['source']['id'] != 'cloud'
|
||||
_r = os.path.exists(_layout['root'])
|
||||
if not _r and (_p or _q) :
|
||||
#
|
||||
# If we did running this app from installed framework (this should not apply to dependent apps)
|
||||
#
|
||||
_root = os.sep.join([_path,_layout['root']])
|
||||
self._config['layout']['root'] = _root
|
||||
self._config['layout']['root_prefix'] = _root
|
||||
|
||||
def init_menu(self):
|
||||
"""
|
||||
This function will read menu and sub-menu items from disk structure,
|
||||
The files are loaded will
|
||||
"""
|
||||
|
||||
_config = self._config
|
||||
if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' :
|
||||
_sourceHandler = cloud
|
||||
else:
|
||||
_sourceHandler = disk
|
||||
_object = _sourceHandler.build(_config)
|
||||
|
||||
#
|
||||
# After building the site's menu, let us add the one from 3rd party apps
|
||||
#
|
||||
|
||||
|
||||
_layout = copy.deepcopy(_config['layout'])
|
||||
_overwrite = _layout['overwrite'] if 'overwrite' in _layout else {}
|
||||
|
||||
#
|
||||
# @TODO: Find a way to translate rename/replace keys of the _object (menu) here
|
||||
#
|
||||
#-- applying overwrites to the menu items
|
||||
for _name in _object :
|
||||
_submenu = _object[_name]
|
||||
_index = 0
|
||||
for _item in _submenu :
|
||||
text = _item['text'].strip()
|
||||
if text in _overwrite :
|
||||
if 'uri' in _item and 'url' in 'url' in _overwrite[text] :
|
||||
del _item['uri']
|
||||
_item = dict(_item,**_overwrite[text])
|
||||
|
||||
if 'uri' in _item and 'type' in _item and _item['type'] != 'open':
|
||||
_item['uri'] = _item['uri'].replace(_layout['root'],'')
|
||||
|
||||
_submenu[_index] = _item
|
||||
_index += 1
|
||||
self.init_apps(_object)
|
||||
self._menu = _object
|
||||
self._order()
|
||||
|
||||
def init_apps (self,_menu):
|
||||
"""
|
||||
Insuring that the apps are loaded into the menu with an approriate label
|
||||
"""
|
||||
_system = self._config['system']
|
||||
_context = _system['context']
|
||||
if 'routes' in _system :
|
||||
# _items = []
|
||||
_overwrite = {} if 'overwrite' not in self._config['layout'] else self._config['layout']['overwrite']
|
||||
for _text in _system['routes'] :
|
||||
_item = _system['routes'][_text]
|
||||
if 'menu' not in _item :
|
||||
continue
|
||||
uri = f'{_context}/{_text}'
|
||||
# _items.append ({"text":_text,'uri':uri,'type':'open'})
|
||||
_label = _item['menu']
|
||||
if _label not in _menu :
|
||||
_menu [_label] = []
|
||||
_menu[_label].append ({"text":_text,'uri':uri,'type':'open'})
|
||||
# _overwrite[_text] = {'text': _text.replace('-',' ').replace('_',' '),'uri':uri,'type':'open'}
|
||||
# _menu['products'] = _items
|
||||
#
|
||||
# given that the menu items assumes redirecting to a page ...
|
||||
# This is not the case
|
||||
#
|
||||
# self._config['overwrite'] = _overwrite
|
||||
else:
|
||||
pass
|
||||
|
||||
pass
|
||||
def _order (self):
|
||||
_config = self._config
|
||||
if 'order' in _config['layout'] and 'menu' in _config['layout']['order']:
|
||||
_sortedmenu = {}
|
||||
_menu = self._menu
|
||||
for _name in _config['layout']['order']['menu'] :
|
||||
if _name in _menu :
|
||||
_sortedmenu[_name] = _menu[_name]
|
||||
|
||||
_menu = _sortedmenu if _sortedmenu else _menu
|
||||
#
|
||||
# If there are missing items in the sorting
|
||||
_missing = list(set(self._menu.keys()) - set(_sortedmenu))
|
||||
if _missing :
|
||||
for _name in _missing :
|
||||
_menu[_name] = self._menu[_name]
|
||||
_config['layout']['menu'] = _menu #cms.components.menu(_config)
|
||||
self._menu = _menu
|
||||
self._config = _config
|
||||
def init_plugins(self) :
|
||||
"""
|
||||
This function looks for plugins in the folder on disk (no cloud support) and attempts to load them
|
||||
"""
|
||||
_config = self._config
|
||||
PATH= os.sep.join([_config['layout']['root'],'_plugins'])
|
||||
if not os.path.exists(PATH) :
|
||||
#
|
||||
# we need to determin if there's an existing
|
||||
PATH = os.sep.join(self._path.split(os.sep)[:-1]+ [PATH] )
|
||||
if not os.path.exists(PATH) and self._location and os.path.exists(self._location) :
|
||||
#
|
||||
# overriding the location of plugins ...
|
||||
PATH = os.sep.join([self._location, _config['layout']['root'],'_plugins'])
|
||||
|
||||
_map = {}
|
||||
# if not os.path.exists(PATH) :
|
||||
# return _map
|
||||
if 'plugins' not in _config :
|
||||
_config['plugins'] = {}
|
||||
_conf = _config['plugins']
|
||||
|
||||
for _key in _conf :
|
||||
|
||||
_path = os.sep.join([PATH,_key+".py"])
|
||||
if not os.path.exists(_path):
|
||||
continue
|
||||
for _name in _conf[_key] :
|
||||
_pointer = self._load_plugin(path=_path,name=_name)
|
||||
if _pointer :
|
||||
_uri = "/".join(["api",_key,_name])
|
||||
_map[_uri] = _pointer
|
||||
#
|
||||
# We are adding some source specific plugins to the user-defined plugins
|
||||
# This is intended to have out-of the box plugins...
|
||||
#
|
||||
if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' :
|
||||
_plugins = cloud.plugins()
|
||||
else:
|
||||
_plugins = disk.plugins()
|
||||
#
|
||||
# If there are any plugins found, we should load them and use them
|
||||
|
||||
if _plugins :
|
||||
_map = dict(_map,**_plugins)
|
||||
else:
|
||||
pass
|
||||
self._plugins = _map
|
||||
self._config['plugins'] = self._plugins
|
||||
|
||||
def _load_plugin(self,**_args):
|
||||
"""
|
||||
This function will load external module form a given location and return a pointer to a function in a given module
|
||||
:path absolute path of the file (considered plugin) to be loaded
|
||||
:name name of the function to be applied
|
||||
"""
|
||||
_path = _args['path'] #os.sep.join([_args['root'],'plugin'])
|
||||
if os.path.isdir(_path):
|
||||
files = os.listdir(_path)
|
||||
if files :
|
||||
files = [name for name in files if name.endswith('.py')]
|
||||
if files:
|
||||
_path = os.sep.join([_path,files[0]])
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
#-- We have a file ...
|
||||
_name = _args['name']
|
||||
|
||||
spec = importlib.util.spec_from_file_location(_name, _path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
return getattr(module,_name) if hasattr(module,_name) else None
|
||||
|
||||
class Getter (Loader):
|
||||
def __init__(self,**_args):
|
||||
super().__init__(**_args)
|
||||
def load(self):
|
||||
super().load()
|
||||
_system = self.system()
|
||||
_logo = _system['logo']
|
||||
if 'source' in _system and 'id' in _system['source'] and (_system['source']['id'] == 'cloud'):
|
||||
|
||||
_icon = f'/api/cloud/download?doc=/{_logo}'
|
||||
_system['icon'] = _icon
|
||||
|
||||
else:
|
||||
_root = self._config['layout']['root']
|
||||
_icon = os.sep.join([_root,_logo])
|
||||
_system['icon'] = _logo
|
||||
|
||||
self._config['system'] = _system
|
||||
if self._caller :
|
||||
_system['caller'] = {'icon': self._caller.system()['icon']}
|
||||
def html(self,uri,id,_args={},_system={}) :
|
||||
"""
|
||||
This function reads a given uri and returns the appropriate html document, and applies environment context
|
||||
|
||||
"""
|
||||
_system = self._config['system']
|
||||
if 'source' in _system and _system['source']['id'] == 'cloud':
|
||||
_html = cloud.html(uri,dict(_args,**{'system':_system}))
|
||||
|
||||
else:
|
||||
|
||||
_html = disk.html(uri,self.layout())
|
||||
# _html = (open(uri)).read()
|
||||
|
||||
|
||||
#return ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>'])
|
||||
_html = ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>'])
|
||||
appContext = Environment(loader=BaseLoader()).from_string(_html)
|
||||
_args['system'] = _system
|
||||
#
|
||||
# If the rendering of the HTML happens here we should plugin custom functions (at the very least)
|
||||
#
|
||||
|
||||
return appContext.render(**_args)
|
||||
# return _html
|
||||
|
||||
def data (self,_args):
|
||||
"""
|
||||
:store data-store parameters (data-transport, github.com/lnyemba/data-transport)
|
||||
:query query to be applied against the store (expected data-frame)
|
||||
"""
|
||||
_store = _args['store']
|
||||
reader = transport.factory.instance(**_store)
|
||||
_queries= copy.deepcopy(_store['query'])
|
||||
_data = reader.read(**_queries)
|
||||
return _data
|
||||
def csv(self,uri) :
|
||||
return pd.read(uri).to_html()
|
||||
|
||||
return _map
|
||||
def menu(self):
|
||||
return self._config['menu']
|
||||
def plugins(self):
|
||||
return copy.deepcopy(self._plugins) if 'plugins' in self._config else {}
|
||||
def context(self):
|
||||
"""
|
||||
adding custom variables functions to Jinja2, this function should be called after plugins are loaded
|
||||
"""
|
||||
_plugins = self.plugins()
|
||||
# if not location:
|
||||
# env = Environment(loader=BaseLoader())
|
||||
# else:
|
||||
location = self._config['layout']['root']
|
||||
# env = Environment(loader=FileSystemLoader(location))
|
||||
env = Environment(loader=BaseLoader())
|
||||
# env.globals['routes'] = _config['plugins']
|
||||
return env
|
||||
def config(self):
|
||||
return copy.deepcopy(self._config)
|
||||
def system(self,skip=[]):
|
||||
"""
|
||||
:skip keys to ignore in the object ...
|
||||
"""
|
||||
_data = copy.deepcopy(self._config['system'])
|
||||
_system = {}
|
||||
if skip and _system:
|
||||
for key in _data.keys() :
|
||||
if key not in skip :
|
||||
_system[key] = _data[key]
|
||||
else:
|
||||
_system= _data
|
||||
return _system
|
||||
def layout(self):
|
||||
return self._config['layout']
|
||||
def get_app(self):
|
||||
return self._config['system']['app']
|
||||
|
||||
|
||||
class Router :
|
||||
def __init__(self,**_args) :
|
||||
|
||||
# _app = Getter (path = path)
|
||||
_app = Getter (**_args)
|
||||
|
||||
|
||||
self._id = 'main'
|
||||
# _app.load()
|
||||
self._apps = {}
|
||||
_system = _app.system()
|
||||
if 'routes' in _system :
|
||||
_system = _system['routes']
|
||||
for _name in _system :
|
||||
_path = _system[_name]['path']
|
||||
self._apps[_name] = Getter(path=_path,caller=_app,location=_path)
|
||||
self._apps['main'] = _app
|
||||
|
||||
def set(self,_id):
|
||||
self._id = _id
|
||||
def get(self):
|
||||
|
||||
return self._apps['main'] if self._id not in self._apps else self._apps[self._id]
|
||||
def get_main(self):
|
||||
return self._apps['main']
|
||||
# class Loader :
|
||||
# """
|
||||
# This class is designed to exclusively load configuration from disk into an object
|
||||
# :path path to the configuraiton file
|
||||
# :location original location (caller)
|
||||
# """
|
||||
# def __init__(self,**_args):
|
||||
# self._path = _args['path']
|
||||
# self._original_location = None if 'location' not in _args else _args['location']
|
||||
# self._location = None
|
||||
# self._caller = None if 'caller' not in _args else _args['caller']
|
||||
# print ([' *** ', self._caller])
|
||||
# self._menu = {}
|
||||
# self._plugins={}
|
||||
# self.load()
|
||||
|
||||
|
||||
# def load(self):
|
||||
# """
|
||||
# This function will load menu (overwrite) and plugins
|
||||
# """
|
||||
# self.init_config()
|
||||
# self.init_menu()
|
||||
# self.init_plugins()
|
||||
# def init_config(self):
|
||||
# """
|
||||
# Initialize & loading configuration from disk
|
||||
# """
|
||||
# f = open (self._path)
|
||||
# self._config = json.loads(f.read())
|
||||
|
||||
# if self._caller :
|
||||
# self._location = self._original_location.split(os.sep) # needed for plugin loading
|
||||
# self._location = os.sep.join(self._location[:-1])
|
||||
# self._config['system']['portal'] = self._caller != None
|
||||
|
||||
# #
|
||||
# # let's see if we have a location for a key (i.e security key) in the configuration
|
||||
# #
|
||||
# self.update_config()
|
||||
# # _system = self._config['system']
|
||||
# # if 'source' in _system and 'key' in _system['source'] :
|
||||
# # _path = _system['source']['key']
|
||||
# # if os.path.exists(_path):
|
||||
# # f = open(_path)
|
||||
# # _system['source']['key'] = f.read()
|
||||
# # f.close()
|
||||
# # self._system = _system
|
||||
# # self._config['system'] = _system
|
||||
# def update_config(self):
|
||||
# """
|
||||
# We are going to update the configuration (source.key, layout.root)
|
||||
# """
|
||||
# _system = self._config['system']
|
||||
# #
|
||||
# # updating security-key that allows the application to update on-demand
|
||||
# if 'source' in _system and 'key' in _system['source'] :
|
||||
# _path = _system['source']['key']
|
||||
# if os.path.exists(_path):
|
||||
# f = open(_path)
|
||||
# _system['source']['key'] = f.read()
|
||||
# f.close()
|
||||
# self._system = _system
|
||||
# self._config['system'] = _system
|
||||
# _layout = self._config['layout']
|
||||
# #
|
||||
# # update root so that the app can be launched from anywhere
|
||||
# # This would help reduce the footprint of the app/framework
|
||||
# _path = os.sep.join(self._path.split(os.sep)[:-1])
|
||||
# _p = 'source' not in _system
|
||||
# _q = 'source' in _system and _system['source']['id'] != 'cloud'
|
||||
# _r = os.path.exists(_layout['root'])
|
||||
# if not _r and (_p or _q) :
|
||||
# #
|
||||
# # If we did running this app from installed framework (this should not apply to dependent apps)
|
||||
# #
|
||||
# _root = os.sep.join([_path,_layout['root']])
|
||||
# self._config['layout']['root'] = _root
|
||||
# self._config['layout']['root_prefix'] = _root
|
||||
|
||||
# def init_menu(self):
|
||||
# """
|
||||
# This function will read menu and sub-menu items from disk structure,
|
||||
# The files are loaded will
|
||||
# """
|
||||
|
||||
# _config = self._config
|
||||
# if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' :
|
||||
# _sourceHandler = cloud
|
||||
# else:
|
||||
# _sourceHandler = disk
|
||||
# _object = _sourceHandler.build(_config)
|
||||
|
||||
# #
|
||||
# # After building the site's menu, let us add the one from 3rd party apps
|
||||
# #
|
||||
|
||||
|
||||
# _layout = copy.deepcopy(_config['layout'])
|
||||
# _overwrite = _layout['overwrite'] if 'overwrite' in _layout else {}
|
||||
|
||||
# #
|
||||
# # @TODO: Find a way to translate rename/replace keys of the _object (menu) here
|
||||
# #
|
||||
# #-- applying overwrites to the menu items
|
||||
# for _name in _object :
|
||||
# _submenu = _object[_name]
|
||||
# _index = 0
|
||||
# for _item in _submenu :
|
||||
# text = _item['text'].strip()
|
||||
# if text in _overwrite :
|
||||
# if 'uri' in _item and 'url' in 'url' in _overwrite[text] :
|
||||
# del _item['uri']
|
||||
# _item = dict(_item,**_overwrite[text])
|
||||
|
||||
# if 'uri' in _item and 'type' in _item and _item['type'] != 'open':
|
||||
# _item['uri'] = _item['uri'].replace(_layout['root'],'')
|
||||
|
||||
# _submenu[_index] = _item
|
||||
# _index += 1
|
||||
# self.init_apps(_object)
|
||||
# self._menu = _object
|
||||
# self._order()
|
||||
|
||||
# def init_apps (self,_menu):
|
||||
# """
|
||||
# Insuring that the apps are loaded into the menu with an approriate label
|
||||
# """
|
||||
# _system = self._config['system']
|
||||
# _context = _system['context']
|
||||
# if 'routes' in _system :
|
||||
# # _items = []
|
||||
# _overwrite = {} if 'overwrite' not in self._config['layout'] else self._config['layout']['overwrite']
|
||||
# for _text in _system['routes'] :
|
||||
# _item = _system['routes'][_text]
|
||||
# if 'menu' not in _item :
|
||||
# continue
|
||||
# uri = f'{_context}/{_text}'
|
||||
# # _items.append ({"text":_text,'uri':uri,'type':'open'})
|
||||
# _label = _item['menu']
|
||||
# if _label not in _menu :
|
||||
# _menu [_label] = []
|
||||
# _menu[_label].append ({"text":_text,'uri':uri,'type':'open'})
|
||||
# # _overwrite[_text] = {'text': _text.replace('-',' ').replace('_',' '),'uri':uri,'type':'open'}
|
||||
# # _menu['products'] = _items
|
||||
# #
|
||||
# # given that the menu items assumes redirecting to a page ...
|
||||
# # This is not the case
|
||||
# #
|
||||
# # self._config['overwrite'] = _overwrite
|
||||
# else:
|
||||
# pass
|
||||
|
||||
# pass
|
||||
# def _order (self):
|
||||
# _config = self._config
|
||||
# if 'order' in _config['layout'] and 'menu' in _config['layout']['order']:
|
||||
# _sortedmenu = {}
|
||||
# _menu = self._menu
|
||||
# for _name in _config['layout']['order']['menu'] :
|
||||
# if _name in _menu :
|
||||
# _sortedmenu[_name] = _menu[_name]
|
||||
|
||||
# _menu = _sortedmenu if _sortedmenu else _menu
|
||||
# #
|
||||
# # If there are missing items in the sorting
|
||||
# _missing = list(set(self._menu.keys()) - set(_sortedmenu))
|
||||
# if _missing :
|
||||
# for _name in _missing :
|
||||
# _menu[_name] = self._menu[_name]
|
||||
# _config['layout']['menu'] = _menu #cms.components.menu(_config)
|
||||
# self._menu = _menu
|
||||
# self._config = _config
|
||||
# def init_plugins(self) :
|
||||
# """
|
||||
# This function looks for plugins in the folder on disk (no cloud support) and attempts to load them
|
||||
# """
|
||||
# _config = self._config
|
||||
# PATH= os.sep.join([_config['layout']['root'],'_plugins'])
|
||||
# if not os.path.exists(PATH) :
|
||||
# #
|
||||
# # we need to determin if there's an existing
|
||||
# PATH = os.sep.join(self._path.split(os.sep)[:-1]+ [PATH] )
|
||||
# if not os.path.exists(PATH) and self._location and os.path.exists(self._location) :
|
||||
# #
|
||||
# # overriding the location of plugins ...
|
||||
# PATH = os.sep.join([self._location, _config['layout']['root'],'_plugins'])
|
||||
|
||||
# _map = {}
|
||||
# # if not os.path.exists(PATH) :
|
||||
# # return _map
|
||||
# if 'plugins' not in _config :
|
||||
# _config['plugins'] = {}
|
||||
# _conf = _config['plugins']
|
||||
|
||||
# for _key in _conf :
|
||||
|
||||
# _path = os.sep.join([PATH,_key+".py"])
|
||||
# if not os.path.exists(_path):
|
||||
# continue
|
||||
# for _name in _conf[_key] :
|
||||
# _pointer = self._load_plugin(path=_path,name=_name)
|
||||
# if _pointer :
|
||||
# _uri = "/".join(["api",_key,_name])
|
||||
# _map[_uri] = _pointer
|
||||
# #
|
||||
# # We are adding some source specific plugins to the user-defined plugins
|
||||
# # This is intended to have out-of the box plugins...
|
||||
# #
|
||||
# if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' :
|
||||
# _plugins = cloud.plugins()
|
||||
# else:
|
||||
# _plugins = disk.plugins()
|
||||
# #
|
||||
# # If there are any plugins found, we should load them and use them
|
||||
|
||||
# if _plugins :
|
||||
# _map = dict(_map,**_plugins)
|
||||
# else:
|
||||
# pass
|
||||
# self._plugins = _map
|
||||
# self._config['plugins'] = self._plugins
|
||||
|
||||
# def _load_plugin(self,**_args):
|
||||
# """
|
||||
# This function will load external module form a given location and return a pointer to a function in a given module
|
||||
# :path absolute path of the file (considered plugin) to be loaded
|
||||
# :name name of the function to be applied
|
||||
# """
|
||||
# _path = _args['path'] #os.sep.join([_args['root'],'plugin'])
|
||||
# if os.path.isdir(_path):
|
||||
# files = os.listdir(_path)
|
||||
# if files :
|
||||
# files = [name for name in files if name.endswith('.py')]
|
||||
# if files:
|
||||
# _path = os.sep.join([_path,files[0]])
|
||||
# else:
|
||||
# return None
|
||||
# else:
|
||||
# return None
|
||||
# #-- We have a file ...
|
||||
# _name = _args['name']
|
||||
|
||||
# spec = importlib.util.spec_from_file_location(_name, _path)
|
||||
# module = importlib.util.module_from_spec(spec)
|
||||
# spec.loader.exec_module(module)
|
||||
|
||||
# return getattr(module,_name) if hasattr(module,_name) else None
|
||||
|
||||
# class Getter (Loader):
|
||||
# def __init__(self,**_args):
|
||||
# super().__init__(**_args)
|
||||
# def load(self):
|
||||
# super().load()
|
||||
# _system = self.system()
|
||||
# _logo = _system['logo']
|
||||
# if 'source' in _system and 'id' in _system['source'] and (_system['source']['id'] == 'cloud'):
|
||||
|
||||
# _icon = f'/api/cloud/download?doc=/{_logo}'
|
||||
# _system['icon'] = _icon
|
||||
|
||||
# else:
|
||||
# _root = self._config['layout']['root']
|
||||
# _icon = os.sep.join([_root,_logo])
|
||||
# _system['icon'] = _logo
|
||||
|
||||
# self._config['system'] = _system
|
||||
# if self._caller :
|
||||
# _system['caller'] = {'icon': self._caller.system()['icon']}
|
||||
# def html(self,uri,id,_args={},_system={}) :
|
||||
# """
|
||||
# This function reads a given uri and returns the appropriate html document, and applies environment context
|
||||
|
||||
# """
|
||||
# _system = self._config['system']
|
||||
# if 'source' in _system and _system['source']['id'] == 'cloud':
|
||||
# _html = cloud.html(uri,dict(_args,**{'system':_system}))
|
||||
|
||||
# else:
|
||||
|
||||
# _html = disk.html(uri,self.layout())
|
||||
# # _html = (open(uri)).read()
|
||||
|
||||
|
||||
# #return ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>'])
|
||||
# _html = ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>'])
|
||||
# appContext = Environment(loader=BaseLoader()).from_string(_html)
|
||||
# _args['system'] = _system
|
||||
# #
|
||||
# # If the rendering of the HTML happens here we should plugin custom functions (at the very least)
|
||||
# #
|
||||
|
||||
# return appContext.render(**_args)
|
||||
# # return _html
|
||||
|
||||
# def data (self,_args):
|
||||
# """
|
||||
# :store data-store parameters (data-transport, github.com/lnyemba/data-transport)
|
||||
# :query query to be applied against the store (expected data-frame)
|
||||
# """
|
||||
# _store = _args['store']
|
||||
# reader = transport.factory.instance(**_store)
|
||||
# _queries= copy.deepcopy(_store['query'])
|
||||
# _data = reader.read(**_queries)
|
||||
# return _data
|
||||
# def csv(self,uri) :
|
||||
# return pd.read(uri).to_html()
|
||||
|
||||
# return _map
|
||||
# def menu(self):
|
||||
# return self._config['menu']
|
||||
# def plugins(self):
|
||||
# return copy.deepcopy(self._plugins) if 'plugins' in self._config else {}
|
||||
# def context(self):
|
||||
# """
|
||||
# adding custom variables functions to Jinja2, this function should be called after plugins are loaded
|
||||
# """
|
||||
# _plugins = self.plugins()
|
||||
# # if not location:
|
||||
# # env = Environment(loader=BaseLoader())
|
||||
# # else:
|
||||
# location = self._config['layout']['root']
|
||||
# # env = Environment(loader=FileSystemLoader(location))
|
||||
# env = Environment(loader=BaseLoader())
|
||||
# # env.globals['routes'] = _config['plugins']
|
||||
# return env
|
||||
# def config(self):
|
||||
# return copy.deepcopy(self._config)
|
||||
# def system(self,skip=[]):
|
||||
# """
|
||||
# :skip keys to ignore in the object ...
|
||||
# """
|
||||
# _data = copy.deepcopy(self._config['system'])
|
||||
# _system = {}
|
||||
# if skip and _system:
|
||||
# for key in _data.keys() :
|
||||
# if key not in skip :
|
||||
# _system[key] = _data[key]
|
||||
# else:
|
||||
# _system= _data
|
||||
# return _system
|
||||
# def layout(self):
|
||||
# return self._config['layout']
|
||||
# def get_app(self):
|
||||
# return self._config['system']['app']
|
||||
|
||||
|
||||
# class Router :
|
||||
# def __init__(self,**_args) :
|
||||
|
||||
# # _app = Getter (path = path)
|
||||
# _app = Getter (**_args)
|
||||
|
||||
|
||||
# self._id = 'main'
|
||||
# # _app.load()
|
||||
# self._apps = {}
|
||||
# _system = _app.system()
|
||||
# if 'routes' in _system :
|
||||
# _system = _system['routes']
|
||||
# for _name in _system :
|
||||
# _path = _system[_name]['path']
|
||||
# self._apps[_name] = Getter(path=_path,caller=_app,location=_path)
|
||||
# self._apps['main'] = _app
|
||||
|
||||
# def set(self,_id):
|
||||
# self._id = _id
|
||||
# def get(self):
|
||||
|
||||
# return self._apps['main'] if self._id not in self._apps else self._apps[self._id]
|
||||
# def get_main(self):
|
||||
# return self._apps['main']
|
||||
|
@ -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
|
@ -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;
|
||||
}
|
@ -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;}
|
@ -1,11 +1,9 @@
|
||||
|
||||
{% if layout.header.logo == True %}
|
||||
<div class="icon">
|
||||
<img src="{{system.icon}}">
|
||||
</div>
|
||||
<div class="icon">
|
||||
<img src="{{system.icon}}">
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
<div>
|
||||
<div class="title">{{layout.header.title}}</div>
|
||||
<div class="subtitle">{{layout.header.subtitle}}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="title">{{layout.header.title}}</div>
|
||||
<div class="subtitle">{{layout.header.subtitle}}</div>
|
||||
</div>
|
@ -1,4 +1,15 @@
|
||||
__author__ = "Steve L. Nyemba<steve@the-phi.com>"
|
||||
__version__= "2.0"
|
||||
__author__ = "Steve L. Nyemba"
|
||||
__version__= "2.1.6"
|
||||
__email__ = "steve@the-phi.com"
|
||||
__license__="""
|
||||
Copyright 2010 - 2024, Steve L. Nyemba, Vanderbilt University Medical Center
|
||||
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
@ -0,0 +1,44 @@
|
||||
### What is QCMS
|
||||
|
||||
QCMS is a lightweight, flask CMS designed easy to get working and allowing users to be able to change/edit content using **nextcloud** or **owncloud** as they see fit if need be.
|
||||
|
||||
QCMS comes with standard web frameworks and python frameworks out of the box
|
||||
|
||||
|HTML/JS| Python Frameworks|
|
||||
|---|---|
|
||||
|FontAwesome<br>JQuery<br>ApexCharts<br> | 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…
Reference in new issue