Merge pull request 'v2.0' (#9) from v2.0 into master

Reviewed-on: cloud/cms#9
master
Steve L. Nyemba 3 months ago
commit eb2a8d7146

@ -0,0 +1,2 @@
recursive-include cms/templates *
recursive-include cms/static *

@ -0,0 +1,354 @@
#!/usr/bin/env python
import numpy as np
import os
import sys
import json
import importlib
import importlib.util
from git.repo.base import Repo
import typer
from typing_extensions import Annotated
from typing import Optional
import meta
import uuid
from termcolor import colored
import base64
import io
from cms import index
start = index.start
__doc__ = """
(c) 2022 Quick Content Management System - QCMS
Health Information Privacy Lab - Vanderbilt University Medical Center
MIT License
Is a Python/Flask template for creating websites using disk structure to build the site and loading custom code
The basic template for a flask application will be created in a specified location, within this location will be found a folder "content"
- within the folder specify the folder structure that constitute the menu
Usage :
qcms --create <path> --version <value> --title <title> --subtitle <subtitle>
"""
PASSED = ' '.join(['[',colored(u'\u2713', 'green'),']'])
FAILED= ' '.join(['[',colored(u'\u2717','red'),']'])
INVALID_FOLDER = """
{FAILED} Unable to proceed, could not find project manifest. It should be qcms-manifest.json
"""
#
# handling cli interface
cli = typer.Typer()
def _get_config (path) :
if os.path.exists(path) :
f = open(path)
_conf = json.loads(f.read())
f.close()
else:
_conf = {}
return _conf
def _isvalid(_allowed,**_args):
if not list(set(_allowed) - set(_args.keys())) :
_pargs = {}
for key in _allowed :
_pargs [key] = _args[key]
return _pargs
return False
def write_config(_config, path):
f = open(path,'w')
f.write( json.dumps(_config)) ;
f.close()
def _system(**_args):
"""
Both version and context must be provided otherwise they are ignored
:version
:context context
"""
_info = _isvalid(['version','context'],**_args)
_info['theme'] = 'default.css'
_info = _info if _info else {'version':'0.0.0','context':'','theme':'default.css'}
if _info :
_info['logo'] = None if 'logo' not in _args else _args['logo']
#
# There is an aggregation entry here in app
_appInfo = {'debug':True,'port':8084,'threaded':True,'host':'0.0.0.0'}
if 'app' not in _args:
_info['app'] = _appInfo
else:
_info['app'] = dict(_appInfo,**_args['app'])
return _info
def _header(**_args):
return _isvalid(['logo','title','subtitle'],**_args)
def _layout(**_args):
_info = _isvalid(['root','index'],**_args)
_info['on'] = {"load":{},"error":{}}
_url = 'qcms.co'
_overwrite = {"folder1":{"type":"redirect","url":_url},"folder2":{"type":"dialog"},"folder3":{"type":"dialog","url":_url}}
_info['icons'] = {"comment":"use folder names as keys and fontawesome type as values to add icons to menu"}
_info["api"] = {"comment":"use keys as uri and function calls as values"}
_info['map'] = {},
_info['order'] = {'menu':[]}
_info['overwrite'] = _overwrite
return _info
def make (**_args):
"""
:context
:port port to be served
:title title of the application
:root folder of the content to be scanned
"""
if 'port' in _args :
_args['app'] = {'port':_args['port']}
del _args['port']
if 'context' not in _args :
_args['context'] = ''
_info ={'system': _system(**_args)}
_hargs = {'title':'QCMS'} if 'title' not in _args else {'title':_args['title']}
_hargs['subtitle'] = '' if 'subtitle' not in _args else _args['subtitle']
_hargs['logo'] = True
_info['layout'] = _layout(root=_args['root'],index='index.html')
_info['layout']['header'] = _header(**_hargs)
#
if 'footer' in _args :
_info['layout']['footer'] = [{'text':_args['footer']}] if type(_args['footer']) != list else [{'text':term} for term in _args['footeer']]
else:
_info['layout']['footer'] = [{'text':'Vanderbilt University Medical Center'}]
return _info
@cli.command(name="info")
def _info():
"""
This function returns metadata information about this program, no parameters are needed
"""
print ()
print (meta.__name__,meta.__version__)
print (meta.__author__,meta.__email__)
print ()
@cli.command(name='set-app')
# def set_app (host:str="0.0.0.0",context:str="",port:int=8084,debug=True):
def set_app (host:Annotated[str,typer.Argument(help="bind host IP address")]="0.0.0.0",
context:Annotated[str,typer.Argument(help="if behind a proxy server (no forward slash needed)")]="",
port:Annotated[int,typer.Argument(help="port on which to run the application")]=8084,
debug:Annotated[bool,typer.Argument(help="set debug mode on|off")]=True):
"""
This function consists in editing application access i.e port, debug, and/or context
:context alias or path for applications living behind proxy server
"""
global INVALID_FOLDER
_config = _get_config()
if _config :
_app = _config['system']['app']
_app['host'] = host
_app['port'] = port
_app['debug'] = debug
_config['system']['context'] = context
_config['app'] = _app
write_config(_config)
_msg = f"""{PASSED} Successful update, good job !
"""
else:
_msg = INVALID_FOLDER
print (_msg)
@cli.command(name='set-cloud')
# def set_cloud(path:str): #url:str,uid:str,token:str):
def set_cloud(path:Annotated[str,typer.Argument(help="path of the auth-file for the cloud")]): #url:str,uid:str,token:str):
"""
This function overwrites or sets token information for nextcloud. This function will load the content from the cloud instead of locally
:url url of the nextcloud
:uid account identifier
:token token to be used to signin
"""
if os.path.exists(path):
f = open (path)
_auth = json.loads(f.read())
if not set(['url','token','uid']) - set(_auth.keys()) :
url = _auth['url']
if os.path.exists('qcms-manifest.json') :
_config = _get_config()
_config['system']['source'] = path #{'id':'cloud','auth':{'url':url,'uid':uid,'token':token}}
write_config(_config)
title = _config['layout']['header']['title']
_msg = f"""
Successfully update, good job!
{url}
"""
else:
_msg = INVALID_FOLDER
else:
_msg = """NOT A VALID NEXTCLOUD FILE"""
else:
_msg = INVALID_FOLDER
print (_msg)
@cli.command(name='set-key')
# def set_key(path):
def set_key(
manifest:Annotated[str,typer.Argument(help="path of the manifest file")],
keyfile:Annotated[str,typer.Argument(help="path of the key file to generate")]):
"""
create a security key to control the application during developement. The location must have been created prior.
"""
if not os.path.exists(keyfile):
f = open(keyfile,'w')
f.write(str(uuid.uuid4()))
f.close()
#
_config = _get_config(manifest)
if 'source' not in _config['system']:
_config['system']['source'] = {'id':'disk'}
_config['system']['source']['key'] = keyfile
write_config(_config,manifest)
_msg = f"""{PASSED} A key was generated and written to {keyfile}
use this key in header to enable reload of the site ...`
"""
else:
_msg = f"""{FAILED} Could NOT generate a key, because it would seem you already have one
Please manually delete {keyfile}
"""
print (_msg)
def load(**_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
@cli.command (name='create')
# def create(folder:str,root:str,index:str='index.html',title:str='qcms',subtitle:str='',footer:str='Quick Content Management System',version:str='0.1'):
def create(folder:Annotated[str,typer.Argument(help="path of the project folder")],
root:Annotated[str,typer.Argument(help="folder of the content (inside the project folder)")],
index:str=typer.Option(default="index.html",help="index file (markup or html)"), #Annotated[str,typer.Argument(help="index file to load (.html or markup)")]='index.html',
title:str=typer.Option(default="QCMS PROJECT", help="name of the project") , #Annotated[str,typer.Argument(help="title of the project")]='QCMS - TITLE',
subtitle:str=typer.Option(default="designed & built by The Phi Technology",help="tag line about the project (sub-title)"), #Annotated[str,typer.Argument(help="subtitle of the project")]='qcms - subtitle',
footer:str=typer.Option(default="Quick Content Management System",help="text to be placed as footer"), #Annotated[str,typer.Argument(help="text on the footer of the main page")]='Quick Content Management System',
version:str=typer.Option(default="0.2",help="version number") #Annotated[str,typer.Argument(help="version of the site")]='0.1'
):
"""
This function will create a project folder by performing a git clone (pull the template project)
and adding a configuration file to that will bootup the project
folder project folder
root name/location of root folder associated with the web application
@TODO:
- add port,
"""
#global SYS_ARGS
#folder = SYS_ARGS['create']
url = "https://dev.the-phi.com/git/cloud/cms.git"
#if 'url' in SYS_ARGS :
# url = SYS_ARGS['url']
print ("\tcreating project into folder " +folder)
#
# if standalone, then we should create the git repository
# if standalone :
# _repo = Repo.clone_from(url,folder)
# #
# # removing remote folder associated with the project
# _repo.delete_remote(remote='origin')
# else:
# os.makedirs(folder)
#
# @TODO: root should be handled in
rootpath = os.sep.join([folder,root])
_img = None
if not os.path.exists(rootpath) :
print (f"{PASSED} Creating content folder "+rootpath)
os.makedirs(rootpath)
os.makedirs(os.sep.join([rootpath,'_images']))
_img = """data:image/png;base64,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"""
_,_img = _img.split(',',1)
logo = os.sep.join([rootpath,'_images','logo.png'])
f = open(logo,'wb')
f.write(base64.b64decode(_img))
f.close()
_html = __doc__.replace("<","&lt;").replace(">","&gt;").replace("Usage","<br>Usage")
if not os.path.exists(os.sep.join([rootpath,'index.html'])) :
f = open(os.sep.join([rootpath,'index.html']),'w')
f.write(_html)
f.close()
print (f'{PASSED} configuration being written to project folder')
_args = {'title':title,'subtitle':subtitle,'version':version,'root':root,'index':index}
if footer :
_args['footer'] = footer
_config = make(**_args)
#
# updating logo reference (default for now)
_config['system']['logo'] = os.sep.join([_config['layout']['root'],'_images/logo.png'])
f = open(os.sep.join([folder,'qcms-manifest.json']),'w')
f.write(json.dumps(_config))
f.close()
return _config
@cli.command (name='reload')
def reload (path) :
if os.path.exists (path):
pass
@cli.command(name="bootup")
def bootup (
path:Annotated[str,typer.Argument(help="path of the manifest file")]='qcms-manifest.json',
port:int=typer.Option(default=None, help="port number to serve on (will override configuration)")
):
"""
This function will launch a site/project given the location of the manifest file
"""
index.start(path,port)
# if not port :
# index.start(path)
# else:
# index.start(path,port)
def reset():
"""
"""
global SYS_ARGS
if __name__ == '__main__':
cli()

@ -6,192 +6,192 @@ import copy
from jinja2 import Environment, BaseLoader, FileSystemLoader
import importlib
import importlib.util
from cms import disk, cloud, engine
# from cms import disk, cloud, engine
# import cloud
# import index
# class components :
# # @staticmethod
# # def folders (_path):
# # """
# # This function reads the content of a folder (no depth, it must be simple)
# # """
# # _content = os.listdir(_path)
# # return [_name for _name in _content if os.path.isdir(os.sep.join([_path,_name])) if not _name.startswith('_')]
# # @staticmethod
# # def content (_folder) :
# # if os.path.exists(_folder) :
# # # return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_folder,_name])} for _name in os.listdir(_folder) if not _name.startswith('_') and os.path.isfile( os.sep.join([_folder,_name]))]
# # return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_folder,_name])} for _name in os.listdir(_folder) if not _name.startswith('_') and os.path.isfile( os.sep.join([_folder,_name]))]
# # else:
# # return []
# @staticmethod
# def menu(_config):
# """
# This function will read menu and sub-menu items from disk structure,
# The files are loaded will
# """
# # _items = components.folders(_path)
# # _layout = copy.deepcopy(_config['layout'])
# # _overwrite = _layout['overwrite'] if 'overwrite' in _layout else {}
# #
# # content of each menu item
# # _subItems = [ components.content (os.sep.join([_path,_name]))for _name in _items ]
# # if 'map' in _layout :
# # _items = [_name if _name not in _layout['map'] else _layout['map'][_name] for _name in _items]
# # _object = dict(zip(_items,_subItems))
# if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' :
# _sourceHandler = cloud
# else:
# _sourceHandler = disk
# _object = _sourceHandler.build(_config)
# # _object = disk.build(_path,_config) if type(_path) == str else cloud.build(_path,_config)
# _layout = copy.deepcopy(_config['layout'])
# _overwrite = _layout['overwrite'] if 'overwrite' in _layout else {}
# #
# # @TODO: Find a way to translate rename/replace keys of the _object (menu) here
# #
# #-- applying overwrites to the menu items
# for _name in _object :
# _submenu = _object[_name]
# _index = 0
# for _item in _submenu :
# text = _item['text'].strip()
# if text in _overwrite :
# if 'uri' in _item and 'url' in 'url' in _overwrite[text] :
# del _item['uri']
# _item = dict(_item,**_overwrite[text])
# if 'uri' in _item:
# _item['uri'] = _item['uri'].replace(_layout['root'],'')
# _submenu[_index] = _item
# _index += 1
# return _object
class components :
# @staticmethod
# def folders (_path):
# def html(uri,id,_args={},_system={}) :
# """
# This function reads the content of a folder (no depth, it must be simple)
# This function reads a given uri and returns the appropriate html document, and applies environment context
# """
# _content = os.listdir(_path)
# return [_name for _name in _content if os.path.isdir(os.sep.join([_path,_name])) if not _name.startswith('_')]
# if 'source' in _system and _system['source']['id'] == 'cloud':
# _html = cloud.html(uri,dict(_args,**{'system':_system}))
# else:
# _html = disk.html(uri)
# # _html = (open(uri)).read()
# #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)
# #
# # If the rendering of the HTML happens here we should plugin custom functions (at the very least)
# #
# return appContext.render(**_args)
# # return _html
# @staticmethod
# def content (_folder) :
# if os.path.exists(_folder) :
# # return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_folder,_name])} for _name in os.listdir(_folder) if not _name.startswith('_') and os.path.isfile( os.sep.join([_folder,_name]))]
# return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_folder,_name])} for _name in os.listdir(_folder) if not _name.startswith('_') and os.path.isfile( os.sep.join([_folder,_name]))]
# def data (_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
# @staticmethod
# def csv(uri) :
# return pd.read(uri).to_html()
# @staticmethod
# def load_plugin(**_args):
# """
# This function will load external module form a given location and return a pointer to a function in a given module
# :path absolute path of the file (considered plugin) to be loaded
# :name name of the function to be applied
# """
# _path = _args['path'] #os.sep.join([_args['root'],'plugin'])
# if os.path.isdir(_path):
# files = os.listdir(_path)
# if files :
# files = [name for name in files if name.endswith('.py')]
# if files:
# _path = os.sep.join([_path,files[0]])
# else:
# return []
@staticmethod
def menu(_config):
"""
This function will read menu and sub-menu items from disk structure,
The files are loaded will
"""
# _items = components.folders(_path)
# return None
# else:
# return None
# #-- We have a file ...
# _name = _args['name']
# _layout = copy.deepcopy(_config['layout'])
# _overwrite = _layout['overwrite'] if 'overwrite' in _layout else {}
#
# content of each menu item
# _subItems = [ components.content (os.sep.join([_path,_name]))for _name in _items ]
# if 'map' in _layout :
# _items = [_name if _name not in _layout['map'] else _layout['map'][_name] for _name in _items]
# _object = dict(zip(_items,_subItems))
if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' :
_sourceHandler = cloud
else:
_sourceHandler = disk
_object = _sourceHandler.build(_config)
# _object = disk.build(_path,_config) if type(_path) == str else cloud.build(_path,_config)
_layout = copy.deepcopy(_config['layout'])
_overwrite = _layout['overwrite'] if 'overwrite' in _layout else {}
#
# @TODO: Find a way to translate rename/replace keys of the _object (menu) here
#
#-- applying overwrites to the menu items
for _name in _object :
_submenu = _object[_name]
_index = 0
for _item in _submenu :
text = _item['text'].strip()
if text in _overwrite :
if 'uri' in _item and 'url' in 'url' in _overwrite[text] :
del _item['uri']
_item = dict(_item,**_overwrite[text])
if 'uri' in _item:
_item['uri'] = _item['uri'].replace(_layout['root'],'')
_submenu[_index] = _item
_index += 1
return _object
@staticmethod
def html(uri,id,_args={},_system={}) :
"""
This function reads a given uri and returns the appropriate html document, and applies environment context
"""
if 'source' in _system and _system['source']['id'] == 'cloud':
_html = cloud.html(uri,dict(_args,**{'system':_system}))
else:
_html = disk.html(uri)
# _html = (open(uri)).read()
#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)
#
# If the rendering of the HTML happens here we should plugin custom functions (at the very least)
#
return appContext.render(**_args)
# return _html
@staticmethod
def data (_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
@staticmethod
def csv(uri) :
return pd.read(uri).to_html()
@staticmethod
def load_plugin(**_args):
"""
This function will load external module form a given location and return a pointer to a function in a given module
:path absolute path of the file (considered plugin) to be loaded
:name name of the function to be applied
"""
_path = _args['path'] #os.sep.join([_args['root'],'plugin'])
if os.path.isdir(_path):
files = os.listdir(_path)
if files :
files = [name for name in files if name.endswith('.py')]
if files:
_path = os.sep.join([_path,files[0]])
else:
return None
else:
return None
#-- We have a file ...
_name = _args['name']
spec = importlib.util.spec_from_file_location(_name, _path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return getattr(module,_name) if hasattr(module,_name) else None
@staticmethod
def plugins(_config) :
"""
This function looks for plugins in the folder on disk (no cloud support) and attempts to load them
"""
PATH= os.sep.join([_config['layout']['root'],'_plugins'])
_map = {}
# if not os.path.exists(PATH) :
# spec = importlib.util.spec_from_file_location(_name, _path)
# module = importlib.util.module_from_spec(spec)
# spec.loader.exec_module(module)
# return getattr(module,_name) if hasattr(module,_name) else None
# @staticmethod
# def plugins(_config) :
# """
# This function looks for plugins in the folder on disk (no cloud support) and attempts to load them
# """
# PATH= os.sep.join([_config['layout']['root'],'_plugins'])
# _map = {}
# # if not os.path.exists(PATH) :
# # return _map
# if 'plugins' not in _config :
# _config['plugins'] = {}
# _conf = _config['plugins']
# for _key in _conf :
# _path = os.sep.join([PATH,_key+".py"])
# if not os.path.exists(_path):
# continue
# for _name in _conf[_key] :
# _pointer = components.load_plugin(path=_path,name=_name)
# if _pointer :
# _uri = "/".join(["api",_key,_name])
# _map[_uri] = _pointer
# #
# # We are adding some source specific plugins to the user-defined plugins
# # This is intended to have out-of the box plugins...
# #
# if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' :
# _plugins = cloud.plugins()
# else:
# _plugins = disk.plugins()
# #
# # If there are any plugins found, we should load them and use them
# if _plugins :
# _map = dict(_map,**_plugins)
# return _map
if 'plugins' not in _config :
_config['plugins'] = {}
_conf = _config['plugins']
for _key in _conf :
_path = os.sep.join([PATH,_key+".py"])
if not os.path.exists(_path):
continue
for _name in _conf[_key] :
_pointer = components.load_plugin(path=_path,name=_name)
if _pointer :
_uri = "/".join(["api",_key,_name])
_map[_uri] = _pointer
#
# We are adding some source specific plugins to the user-defined plugins
# This is intended to have out-of the box plugins...
#
if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' :
_plugins = cloud.plugins()
else:
_plugins = disk.plugins()
#
# If there are any plugins found, we should load them and use them
if _plugins :
_map = dict(_map,**_plugins)
return _map
@staticmethod
def context(_config):
"""
adding custom variables functions to Jinja2, this function should be called after plugins are loaded
"""
_plugins = _config['plugins']
# if not location:
# @staticmethod
# def context(_config):
# """
# adding custom variables functions to Jinja2, this function should be called after plugins are loaded
# """
# _plugins = _config['plugins']
# # if not location:
# # env = Environment(loader=BaseLoader())
# # else:
# location = _config['layout']['root']
# # env = Environment(loader=FileSystemLoader(location))
# env = Environment(loader=BaseLoader())
# else:
location = _config['layout']['root']
# env = Environment(loader=FileSystemLoader(location))
env = Environment(loader=BaseLoader())
# env.globals['routes'] = _config['plugins']
return env
@staticmethod
def get_system(_config,skip_keys=[]):
_system = copy.deepcopy(_config['system'])
if skip_keys :
for key in skip_keys :
if key in _system :
del _system
return _system
# # env.globals['routes'] = _config['plugins']
# return env
# @staticmethod
# def get_system(_config,skip_keys=[]):
# _system = copy.deepcopy(_config['system'])
# if skip_keys :
# for key in skip_keys :
# if key in _system :
# del _system
# return _system

@ -4,8 +4,8 @@ Reads from nextcloud
import nextcloud_client as nc
import copy
from mistune import markdown
from mistune import markdown #version 0.8.4
import os
import time
import json
@ -26,6 +26,24 @@ def _format_root_folder (_root):
_root = _root[:-1]
return _root.replace('//','/')
def list_files(folder,_config) :
"""
List the content of a folder (html/md) for now
"""
_authfile = _config['system']['source']['auth']
_handler = login(_authfile)
_files = _handler.list(folder,50)
_content = []
for _item in _files :
if _item.file_type == 'file' and _item.get_content_type() in ['text/markdown','text/html'] :
_uri = '/'.join(_item.path.split('/')[2:])
_uri = _item.path
# _content.append({'text':_item.name.split('.')[0],'uri':_uri})
_content.append(_item.name)
return _content
def content(_args):
"""
:url
@ -49,10 +67,18 @@ def content(_args):
_menu = {} #dict.fromkeys(_menu,[])
for _item in _files :
_folder = _item.path.split(_item.name)[0].strip()
_folder = _folder.replace(_root,'').replace('/','')
_folder = _folder.replace(_root,'').replace('//','')
#
# The following lines are intended to prevent an irradict recursive read of a folder content
# We want to keep things simple as we build the menu
#
if len (_folder.split('/')) > 2:
continue
else:
_folder = _folder.replace('/','')
if _item.name[0] in ['.','_'] or _folder == '':
continue ;
if _item.file_type == 'file' and _item.get_content_type() in ['text/markdown','text/html'] :
# _folder = _item.path.split(_item.name)[0].strip()
# _folder = _folder.replace(_root,'').replace('//','')
@ -61,12 +87,6 @@ def content(_args):
_folder = _folder.replace('/' ,' ').strip()
if _folder not in _menu :
_menu [_folder] = []
# print ([_item.name,_key, _key in _menu])
# _menuItem = _ref[_key]
# uri = '/'.join([_args['url'],_item.path])
# uri = _item
# print ([_menuItem, _menuItem in _menu])
uri = '/'.join(_item.path.split('/')[2:])
uri = _item.path
_menu[_folder].append({'text':_item.name.split('.')[0],'uri':uri})
@ -95,11 +115,13 @@ def html (uri,_config) :
_handler = login(_config['system']['source']['auth'])
_root = _format_root_folder(_config['layout']['root'])
uri = _format_root_folder (uri)
_context = _config['system']['context']
_prefix = '/'.join (uri.split('/')[:-1])
_link = '/'.join(['{{context}}api/cloud/download?doc='+_prefix,'.attachments.'])
_link = '/'.join(['api/cloud/download?doc='+_prefix,'.attachments.'])
if _context :
_link = f'{_context}/{_link}'
# _link = '/'.join(['api/cloud/download?doc='+_prefix,'_images'])
@ -107,8 +129,10 @@ def html (uri,_config) :
# print ([uri,uri[-2:] ,uri[-2:] in ['md','MD','markdown']])
_handler.logout()
# if uri.endswith('.md'):
_html = _html.replace(_root,('{{context}}api/cloud/download?doc='+_root)).replace('.attachments.', copy.deepcopy(_link))
if not _context :
_html = _html.replace(_root,('api/cloud/download?doc='+_root)).replace('.attachments.', copy.deepcopy(_link))
else:
_html = _html.replace(_root,(f'{_context}api/cloud/download?doc='+_root)).replace('.attachments.', copy.deepcopy(_link))
# _html = _html.replace('<br />','')
return markdown(_html) if uri[-2:] in ['md','MD','Md','mD'] else _html
# def update (_config):
@ -130,11 +154,20 @@ def download(**_args):
else:
_stream = _handler.get_file_contents(_request.args['doc'])
_handler.logout()
return _stream
pass
def plugins ():
def _format (uri,_config) :
"""
This function does nothing but is used to satisfy the demands of a design pattern
@TODO: revisit the design pattern
"""
return uri
def plugins (_context):
"""
This function publishes the plugins associated with this module
"""
return {'api/cloud/download':download}
key = 'api/cloud/download'
if _context :
key = f'{_context}/{key}'
return {key:download}

@ -2,45 +2,155 @@
This file pulls the content from the disk
"""
import os
def folders (_path):
import importlib
import importlib.util
import copy
from mistune import markdown
def folders (_path,_config):
"""
This function reads the content of a folder (no depth, it must be simple)
"""
_content = os.listdir(_path)
return [_name for _name in _content if os.path.isdir(os.sep.join([_path,_name])) if not _name.startswith('_')]
def content(_folder):
def content(_folder,_config,keep=[]):
"""
:content of the folder
"""
_layout = _config['layout']
if 'location' in _layout :
_uri = os.sep.join([_layout['root'] ,_folder.split(os.sep)[-1]])
_path = os.sep.join([_layout['root'],_folder.split(os.sep)[-1]])
else:
_path = _folder
if os.path.exists(_folder) :
_menuItems = os.listdir(_folder)
_menuItems = list_files(_folder,_config) #os.listdir(_folder)
# return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_folder,_name])} for _name in os.listdir(_folder) if not _name.startswith('_') and os.path.isfile( os.sep.join([_folder,_name]))]
return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_folder,_name])} for _name in os.listdir(_folder) if not _name.startswith('_') and os.path.isfile( os.sep.join([_folder,_name]))]
return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_path,_name])} for _name in os.listdir(_folder) if not _name[0] in ['.','_'] and os.path.isfile( os.sep.join([_folder,_name])) and _name.split('.')[-1] in ['html','md']]
else:
return []
def build (_config): #(_path,_content):
def list_files(_folder,_config, keep=[]):
return [name for name in os.listdir(_folder) if name[0] not in ['.','_']]
def build (_config, keep=[]): #(_path,_content):
"""
building the menu for the site given the content is on disk
:path path of the files on disk
:config configuration associated with the
"""
_path = _config['layout']['root']
_items = folders(_path)
_subItems = [ content (os.sep.join([_path,_name]))for _name in _items ]
# 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 ]
_r = {}
for _name in _items :
_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]
# _r = [_r[_key] for _key in _r if len(_r[_key]) > 0]
return _r
# return dict.fromkeys(_items,_subItems)
def html(uri) :
_html = (open(uri)).read()
def _realpath (uri,_config) :
_layout = _config['layout']
_uri = copy.copy(uri)
if 'location' in _layout :
_uri = os.sep.join([_layout['location'],_uri])
return _uri
def _format (uri,_config):
_layout = _config['layout']
if 'location' in _layout :
return 'api/disk/read?uri='+uri
return uri
def read (**_args):
"""
This will read binary files from disk, and allow the location or not to be read
@TODO: add permissions otherwise there can be disk-wide reads
"""
request = _args['request']
_layout = _args['config']['layout']
_uri = request.args['uri'] # if 'location' in _layout :
# _uri = os.sep.join([_layout['location'],_uri])
_uri = _realpath(_uri, _args['config'])
if os.path.exists(_uri):
f = open(_uri,mode='rb')
_stream = f.read()
f.close()
return _stream
return None
def exists(**_args):
_path = _realpath(_args['uri'],_args['config'])
# _layout = _args['config']['layout']
# if 'location' in _layout :
# _path = os.sep.join([_layout['location'],_path])
return os.path.exists(_path)
def html(_uri,_config) :
# _html = (open(uri)).read()
_path = _realpath(_uri,_config)
_context = _config['system']['context']
_html = ( open(_path)).read()
_layout = _config['layout']
if 'location' in _layout :
if not _config :
_api = os.sep.join(['api/disk/read?uri=',_layout['root']])
else:
_api = os.sep.join([f'{_context}/api/disk/read?uri=',_layout['root']])
_html = _html.replace(_layout['root'],_api)
_html = markdown(_html) if _uri[-2:] in ['md','MD','Md','mD'] else _html
return _html
def plugins ():
return {}
def plugins (**_args):
"""
This function will load plugins from disk given where they come from
:path path of the files
:name name of the module
"""
_context = _args['context']
if 'path' not in _args :
key = 'api/disk/read'
if _context :
key = f'{_context}/{key}'
return {key:read}
_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:
_uri = [_path,files[0]]
if _context :
_uri = [_context] + _uri
_path = os.sep.join(_uri)
else:
return None
else:
#
# LOG: not a file
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)
#
# LOG This plugin ....
return getattr(module,_name) if hasattr(module,_name) else None

@ -9,9 +9,14 @@ 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']
@ -21,6 +26,7 @@ class Loader :
self._plugins={}
self.load()
def load(self):
"""
This function will load menu (overwrite) and plugins
@ -35,16 +41,31 @@ class Loader :
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 in the configuration
# 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):
@ -53,6 +74,22 @@ class Loader :
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,
@ -252,7 +289,8 @@ class Getter (Loader):
_html = cloud.html(uri,dict(_args,**{'system':_system}))
else:
_html = disk.html(uri)
_html = disk.html(uri,self.layout())
# _html = (open(uri)).read()
@ -313,14 +351,19 @@ class Getter (Loader):
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) :
path = _args['path']
_app = Getter (path = path)
# _app = Getter (path = path)
_app = Getter (**_args)
self._id = 'main'
# _app.load()
self._apps = {}
@ -330,8 +373,8 @@ class Router :
for _name in _system :
_path = _system[_name]['path']
self._apps[_name] = Getter(path=_path,caller=_app,location=_path)
print ([_name, self._apps[_name].plugins().keys()])
self._apps['main'] = _app
def set(self,_id):
self._id = _id
def get(self):

@ -0,0 +1,433 @@
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
self.reload() #-- this is an initial load of various components
def reload(self):
self._iconfig(**self._args)
self._uconfig(**self._args)
self._isource()
self._imenu()
self._iplugins()
# 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 _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 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 _label not in _menu :
_menu [_label] = []
_menu[_label].append ({"text":_text,'uri':uri,'type':'open'})
#
# update the menu items and the configuration
#
_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': 'caller/main/'+self._caller.system()['icon'].replace(_callerContext,'')}
_context = _callerContext
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
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'
#
#
# self._name = self._config['system']['name'] if 'name' in self._config['system'] else _args['name']
def system (self,skip=[]):
"""
This function returns system attributes without specific components
"""
_data = copy.deepcopy(self._config['system'])
exclude = skip
_system = {}
if exclude and _system:
for key in _data.keys() :
if key not in exclude :
_system[key] = _data[key]
else:
_system= _data
return _system
def layout (self):
return copy.copy(self._config['layout'])
def plugins (self):
return copy.copy(self._config['plugins'])
def config (self):
return copy.copy(self._config)
def app(self):
_system = self.system()
return _system['app']
def set(self,key,value):
"""
This function will update/set an attribute with a given value
:key
"""
_keys = key.split('.')
_found = 0
if _keys[0] in self._config :
_object = self._config[_keys[0]]
for _akey in _object.keys() :
if _akey == _keys[-1] :
_object[_akey] = value
_found = 1
break
#
#
return _found
#
class MicroService (Module):
"""
This is a CMS MicroService class that is capable of initializing a site and exposing Module functions
"""
def __init__(self,**_args):
super().__init__(**_args)
def format(_content,mimetype):
pass
def html (self,uri, id) :
_system = self.system()
_gshandler = self._handler()
#
#@TODO:
# The uri here must be properly formatted, We need to define the conditions for this
#
_html = _gshandler.html(uri,self._config)
return " ".join([f'<div id="{id}" > ',_html, '</div>'])
def context(self):
return Environment(loader=BaseLoader())
def data (self,**_args):
request = _args['request']
def icon (self):
_handler = self._handler()
_uri = self.system()['icon']
return _handler.read(uri=_uri,config=self._config)
class CMS:
"""
This class aggregates microservices and allows the application of a given service (site/app)
"""
def __init__(self,**_args) :
# _app = Getter (path = path)
# _void = MicroService()
_app = MicroService (**_args)
self._id = 'main'
# _app.load()
self._apps = {}
_system = _app.system()
if 'routes' in _system :
_system = _system['routes']
for _name in _system :
_path = _system[_name]['path']
self._apps[_name] = MicroService(context=_name,path=_path,caller=_app,location=_path)
self._apps['main'] = _app
#
# The following are just a simple delegation pattern (it makes the calling code simpler)
#
def config (self):
return self.get().config()
def render(self,_uri,_id,_appid):
# _handler = self.get()
_handler = self._apps[_appid]
_config = _handler.config()
_args = {'layout':_handler.layout()}
if 'plugins' in _config:
_args['routes'] = _config['plugins']
_html = _handler.html(_uri,_id)
_args['system'] = _handler.system(skip=['source','app'])
e = Environment(loader=BaseLoader()).from_string(_html)
_args[_id] = str(e.render(**_args)) #,_args
return _args
def set(self,_id):
self._id = _id
def get(self):
return self._apps['main'] if self._id not in self._apps else self._apps[self._id]
def get_main(self):
return self._apps['main']

@ -0,0 +1,329 @@
__doc__ = """
arguments :
--config path of the configuration otherwise it will look for the default in the working directory
"""
from flask import Flask,render_template,send_from_directory,request, redirect, Response, session
import flask
import transport
from transport import providers
import cms
import sys
import os
import json
import copy
import io
import base64
from jinja2 import Environment, BaseLoader
import typer
from typing_extensions import Annotated
from typing import Optional
import pandas as pd
import uuid
import datetime
from cms import disk, cloud, engine
_app = Flask(__name__)
cli = typer.Typer()
# @_app.route('/favicon.ico')
# def favicon():
# global _route
# _system = _route.get ().system()
# _handler = _route.get()
# _logo =_system['icon'] if 'icon' in _system else 'static/img/logo.svg'
# return _handler.get(_logo)
# # # _root = _route.get().config()['layout']['root']
# # # print ([_system])
# # # if 'source' in _system and 'id' in _system['source'] and (_system['source']['id'] == 'cloud'):
# # # uri = f'/api/cloud/downloads?doc=/{_logo}'
# # # print (['****' , uri])
# # # return redirect(uri,200) #,{'content-type':'application/image'}
# # # else:
# # # return send_from_directory(_root, #_app.root_path, 'static/img'),
# # _logo, mimetype='image/vnd.microsoft.icon')
def _getHandler () :
_id = session.get('app_id','main')
return _route._apps[_id]
def _setHandler (id) :
session['app_id'] = id
@_app.route("/robots.txt")
def robots_txt():
"""
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 = ['''
User-agent: *
Allow: /
''']
if 'routes' in _system :
for _key in _system['routes'] :
_uri = '/'.join(['',_key])
_info.append(f'''
User-agent: *
Allow: {_uri}
''')
# return '\n'.join(_info),200,{'Content-Type':'plain/text'}
return Response('\n'.join(_info), mimetype='text/plain')
@_app.route("/")
def _index ():
# global _config
# global _route
# _handler = _route.get()
# _config = _route.config()
_handler = _getHandler()
_config = _handler.config()
print ([' serving ',session.get('app_id','NA'),_handler.layout()['root']])
# _system = _handler.system()
# _plugins= _handler.plugins()
# _args = {}
# # if 'plugins' in _config :
# # _args['routes']=_config['plugins']
# # _system = cms.components.get_system(_config) #copy.deepcopy(_config['system'])
# _html = ""
_args={'system':_handler.system(skip=['source','app','data']),'layout':_handler.layout()}
try:
uri = os.sep.join([_config['layout']['root'], _config['layout']['index']])
# # _html = _route.get().html(uri,'index',_config,_system)
# _html = _handler.html(uri,'index')
_index_page = "index.html"
_args = _route.render(uri,'index',session.get('app_id','main'))
except Exception as e:
# print ()
print (e)
_index_page = "404.html"
# _args['uri'] = request.base_url
# pass
# # if 'source' in _system :
# # del _system['source']
# _args = {'layout':_config['layout'],'index':_html}
# _args['system'] = _handler.system(skip=['source','app','route'])
return render_template(_index_page,**_args),200 if _index_page != "404.html" else 200
# @_app.route('/id/<uid>')
# def people(uid):
# """
# This function will implement hardened links that can directly "do something"
# """
# global _config
# return "0",200
@_app.route('/dialog')
def _dialog ():
# global _config
global _route
_handler = _route.get()
_uri = request.headers['uri']
_id = request.headers['dom']
# _html = ''.join(["<div style='padding:1%'>",str( e.render(**_args)),'</div>'])
_args = _route.render(_uri,'html',session.get('app_id','main'))
_args['title'] = _id
return render_template('dialog.html',**_args) #title=_id,html=_html)
@_app.route("/caller/<app>/api/<module>/<name>")
def _delegate_call(app,module,name):
global _route
_handler = _route._apps[app]
return _delegate(_handler,module,name)
@_app.route('/api/<module>/<name>')
def _getproxy(module,name) :
"""
This endpoint will load a module and make a function call
:_module entry specified in plugins of the configuration
:_name name of the function to execute
"""
# global _config
# global _route
# _handler = _route.get()
_handler = _getHandler()
return _delegate(_handler,module,name)
def _delegate(_handler,module,name):
global _route
uri = '/'.join(['api',module,name])
# _args = dict(request.args,**{})
# _args['config'] = _handler.config()
_plugins = _handler.plugins()
_context = _handler.system()['context']
if _context :
uri = f'{_context}/{uri}'
if uri not in _plugins :
_data = {}
_code = 404
else:
pointer = _plugins[uri]
# if _args :
# _data = pointer (**_args)
# else:
# _data = pointer()
_data = pointer(request=request,config=_handler.config())
if type(_data) == pd.DataFrame :
_data = _data.to_dict(orient='records')
if type(_data) == list:
_data = json.dumps(_data)
_code = 200 if _data else 500
return _data,_code
@_app.route("/api/<module>/<name>" , methods=['POST'])
def _post (module,name):
# global _config
# global _route
# _handler = _route.get()
_handler = _getHandler()
return _delegate(_handler,module,name)
@_app.route('/version')
def _version ():
global _route
_handler = _route.get()
global _config
return _handler.system()['version']
@_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
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'])
def cms_page():
"""
return the content of a folder formatted for a menu
"""
# global _config
global _route
# _handler = _route.get()
# _config = _handler.config()
_handler = _getHandler()
_config = _handler.config()
# _uri = os.sep.join([_config['layout']['root'],request.headers['uri']])
_uri = request.headers['uri']
if 'dom' not in request.headers :
_id = _uri.split('/')[-1].split('.')[0]
else:
_id = request.headers['dom']
# _args = {'layout':_config['layout']}
# if 'plugins' in _config:
# _args['routes'] = _config['plugins']
# _system = _handler.system() #cms.components.get_system(_config)
# # _html = _handler.html(_uri,_id,_args,_system) #cms.components.html(_uri,_id,_args,_system)
# _html = _handler.html(_uri,_id)
# # _system = cms.components.get_system(_config)
# _args['system'] = _handler.system(skip=['source','app'])
# e = Environment(loader=BaseLoader()).from_string(_html)
# _html = e.render(**_args)
if 'read?uri=' in _uri or 'download?doc=' in _uri :
_uri = _uri.split('=')[1]
_args = _route.render(_uri,_id,session.get('app_id','main'))
return _args[_id],200
# return _html,200
@_app.route('/page')
def _cms_page ():
# global _config
global _route
# _handler = _route.get()
# _config = _handler.config()
_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'))
return _args[_title],200
@_app.route('/set/<id>')
def set(id):
global _route
_setHandler(id)
# _route.set(id)
# _handler = _route.get()
_handler = _getHandler()
_context = _handler.system()['context']
_uri = f'/{_context}'.replace('//','/')
return redirect(_uri)
@_app.route('/<id>')
def _open(id):
global _route
# _handler = _route.get()
_handler = _getHandler()
if id not in _route._apps :
_args = {'config':_handler.config(), 'layout':_handler.layout(),'system':_handler.system(skip=['source','app'])}
return render_template("404.html",**_args)
else:
_setHandler(id)
# _route.set(id)
return _index()
@cli.command()
def start (
path:Annotated[str,typer.Argument(help="path of the manifest file")]='qcms-manifest.json',
port:int=typer.Option(default=None, help="port number to serve on (will override configuration)")
) :
"""
This function is designed to start the application with its associated manifest (configuration) location
:path path to the the manifest
:shared run in shared mode i.e
"""
global _route
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)
# dir(_route)
# _args = _route.get().get_app()
_args = _route.get().app()
if port :
_args['port'] = port
_app.secret_key = str(uuid.uuid4())
_app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(hours=24)
_app.run(**_args)
_status = 'found'
else:
_status = 'not found'
print(f'''
manifest: {path}
status : {_status}
''')
@cli.command(name='help')
def _help() :
pass
if __name__ == '__main__' :
cli()

@ -0,0 +1 @@
.main { font-size:14px; font-weight: lighter;}

@ -49,13 +49,18 @@
background-color: #000000; color:#f3f3f3;
}
blockquote code { margin-top:8px;
/* blockquote code { margin-top:8px;
background:black; color:#f3f3f3;padding:10px; text-wrap: nowrap;
line-height: 2; width:99%;
font-family: 'Courier New', Courier, monospace; font-weight:lighter; font-size:14px;}
.code {
padding:8px; font-family:'Courier New', Courier, monospace; font-weight: lighter;
background-color: #000000; color:#f3f3f3; font-size:14px;
font-family: 'Courier New', Courier, monospace; font-weight:lighter; font-size:14px;} */
.source-code {
font-family:courier; background-color: #000000; COLOR:#ffffff;
padding:8px;
padding-left:10px;
text-wrap: wrap;
width:calc(100% - 40px);
border-left:8px solid #CAD5E0; margin-left:10px; font-weight: bold;
}
.main .content table {border-spacing: 2;}
.main .content table .active {font-size:14px}

@ -0,0 +1,17 @@
.content {
min-height:85vh;
line-height: 1.5;
font-weight:lighter;
font-family: sans-serif;
}
.main .header {
display:grid;
grid-template-columns: 64px auto; gap:4px;
}
.main .header .title {font-weight:bold; font-size:24px; text-transform: capitalize;}
.main .header .subtitle {font-size:14px; text-transform: capitalize;}
.main .header img {height:64px; width:64px}
.main .menu {display:none}
.pane {width:50%; height:100px; background-color: pink;}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

@ -11,13 +11,17 @@ bootup.CMSObserver = function(_sysId,_domId,_fileURI){
this.apply = function (_caller){
var http = HttpClient.instance()
http.setHeader('uri',_fileURI)
if (sessionStorage[_sysId] != null){
var uri = sessionStorage[_sysId]+'/page'
}else{
var uri = '/page'
}
try{
var _domElement = jx.dom.get.instance('div')
_domElement.className = 'busy-loading'
jx.dom.append(_domId, _domElement)
// var _domElement = jx.dom.get.instance('div')
// _domElement.className = 'busy-loading'
// jx.dom.append(_domId, _domElement)
http.post(uri,function(x){
// console.log(jx.dom.exists(_domId))
// var _domElement = jx.dom.get.instance('div')
@ -30,8 +34,21 @@ bootup.CMSObserver = function(_sysId,_domId,_fileURI){
setTimeout(function(){
_domElement.innerHTML = x.responseText
_domElement.className = null
// _domElement.innerHTML = x.responseText
// _domElement.className = null
// $(_domElement).html(x.responseText)
$('#'+_domId).append(x.responseText)
// $(_domElement).attr('class',_domId)
//
// If there is a script associated it must be extracted and executed
// menu.runScript(_domId)
// console.log([_domId, ' **** ',$(_domId + ' script')])
},1500)
@ -63,6 +80,7 @@ bootup.init = function(sys_id,_layout){
var observers =
jx.utils.patterns.visitor(_layout.on.load[_domId], function(_uri){
// _uri = _layout.root_prefix != null? (_layout.root_prefix+_uri) : _uri
return new bootup.CMSObserver(sys_id,_domId,_uri)
})
observers.push(new bootup.finalize(_domId))

@ -0,0 +1,31 @@
var dialog = {}
dialog.context = ''
dialog.show = function(_title,_internalURI,_message,_pointer){
var http = HttpClient.instance()
// http.setData({title:_title,html:_message},'application/json')
var uri = dialog.context+'/dialog'
http.setHeader('dom',_title)
http.setHeader('uri',_internalURI)
http.get(uri,function(x){
$('.jxmodal').remove()
jx.modal.show({html:x.responseText,id:'body'})
if(jx.dom.exists('dialog-message') && _message != null){
jx.dom.set.value('dialog-message',_message)
}
//
// In order to perhaps execute any js script that should have been executed on load ...
//
var scripts = $('.jxmodal script')
jx.utils.patterns.visitor(scripts,function(_item){
if(_item.text.trim().length > 0){
var _routine = eval(_item.text)
//
//@TODO:
// Find a way to add the running function into the page to enable scripts to work
//
}
})
})
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save