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