@ -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("<","<").replace(">",">").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()
|
@ -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;}
|
@ -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;}
|
After Width: | Height: | Size: 1.9 MiB |
After Width: | Height: | Size: 2.5 MiB |
After Width: | Height: | Size: 3.3 MiB |
After Width: | Height: | Size: 1.6 MiB |
After Width: | Height: | Size: 786 KiB |
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 |
After Width: | Height: | Size: 116 KiB |
After Width: | Height: | Size: 396 KiB |
After Width: | Height: | Size: 187 KiB |
@ -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
|
||||||
|
//
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|