From 9b035b9950700bcf8c4416d9332848c0ec5fd59f Mon Sep 17 00:00:00 2001 From: Steve Nyemba Date: Sun, 21 Jul 2024 17:37:33 -0500 Subject: [PATCH 01/18] new files for version 2.1, themes, modular css --- cms/engine/config/__init__.py | 28 ++++++ cms/engine/config/structure.py | 77 +++++++++++++++++ cms/engine/plugins/__init__.py | 42 +++++++++ cms/engine/project/__init__.py | 154 +++++++++++++++++++++++++++++++++ cms/engine/themes/__init__.py | 44 ++++++++++ cms/static/css/dialog.css | 10 +++ cms/static/css/source-code.css | 18 ++++ 7 files changed, 373 insertions(+) create mode 100644 cms/engine/config/__init__.py create mode 100644 cms/engine/config/structure.py create mode 100644 cms/engine/plugins/__init__.py create mode 100644 cms/engine/project/__init__.py create mode 100644 cms/engine/themes/__init__.py create mode 100644 cms/static/css/dialog.css create mode 100644 cms/static/css/source-code.css diff --git a/cms/engine/config/__init__.py b/cms/engine/config/__init__.py new file mode 100644 index 0000000..0523e4d --- /dev/null +++ b/cms/engine/config/__init__.py @@ -0,0 +1,28 @@ +""" +This file handles all things configuration i.e even the parts of the configuration we are interested in +""" +import os +import json + +def get (path) : + if os.path.exists(path) : + f = open(path) + _conf = json.loads(f.read()) + f.close() + else: + _conf = {} + return _conf +def _isvalid(_allowed,**_args): + + if not list(set(_allowed) - set(_args.keys())) : + _pargs = {} + for key in _allowed : + _pargs [key] = _args[key] + return _pargs + return False + +def write(_config, path): + f = open(path,'w') + f.write( json.dumps(_config)) ; + f.close() + diff --git a/cms/engine/config/structure.py b/cms/engine/config/structure.py new file mode 100644 index 0000000..93aca9c --- /dev/null +++ b/cms/engine/config/structure.py @@ -0,0 +1,77 @@ +""" +This file handles the structure (strict) of the configuration file, +Not all elements are programmatically editable (for now) +""" +import meta +class Section : + + def build (self,**_args): + _data = {} + for _attr in dir(self): + if not _attr.startswith('_') and _attr not in ['build','update']: + _kwargs = _args if _attr not in _args or type(_args[_attr]) !=dict else _args[_attr] + _data[_attr] = getattr(self,_attr)(**_kwargs) + _name = type (self).__name__.lower() + + return {_name:_data } + + + + def update(self,_default,**_args) : + for _attr in _default.keys(): + + if _attr in _args : + _default[_attr] = _args[_attr] + return _default + +class System (Section) : + + def logo(self,**_args): + + return 'www/html/_assets/images/logo.png' if 'logo' not in _args else _args['logo'] + def theme (self,**_args): + """ + setting the theme given the name of the theme + :name name of the theme to set (optional) + """ + return 'default' if 'name' not in _args else _args['name'] + def version(self,**_args): + return meta.__version__ if 'version' not in _args else _args['version'] + def context (self,**_args): + return "" if 'context' not in _args else _args['context'] + def app (self,**_args): + _data = {'debug':True,'port':8084,'threaded':True,'host':'0.0.0.0'} + return self.update(_data,**_args) + + def source(self,**_args): + _data = {'id':'disk'} + if set(['key','auth']) & set(_args.keys()): + # + # key: reboot the app & auth: for cloud access + # + for _attr in ['key','auth'] : + if _attr in _args: + _data[_attr] = _args[_attr] + + _data = self.update(_data,**_args) + return _data + +class Layout (Section): + def index (self,**_args): + return "index.html" if 'index' not in _args else _args['index'] + def root(self,**_args): + return 'wwww/html' if 'root' not in _args else _args['root'] + def footer(self,**_args): + return [{'text':'Powered by QCMS'}] if 'footer' not in _args else _args['footer'] + def on(self,**_args): + return {'load':{}} if 'on' not in _args else _args['on'] + def order(self,**_args): + return {'menu':[]} if 'order' not in _args else _args['order'] + # def menu (self,**_args): + # return {'menu':[]} if 'menu' not in _args else _args['menu'] + def overwrite(self,**_args): + return {} + def header (self,**_args): + _data = {"title":"QCMS Project", "subtitle":"Powered by Python Flask"} + return self.update(_data,**_args) + \ No newline at end of file diff --git a/cms/engine/plugins/__init__.py b/cms/engine/plugins/__init__.py new file mode 100644 index 0000000..6ada10b --- /dev/null +++ b/cms/engine/plugins/__init__.py @@ -0,0 +1,42 @@ +import json +import pandas as pd +import importlib +import importlib.util +import os + +def stats (_config) : + """ + Returns the statistics of the plugins + """ + _data = [] + for _name in _config : + _log = {"files":_name,"loaded":len(_config[_name]),"logs":json.dumps(_config[_name])} + _data.append(_log) + return pd.DataFrame(_data) + pass +def load(_path,_filename,_name) : + """ + This function will load external module form a given location and return a pointer to a function in a given module + :path absolute path of the file (considered plugin) to be loaded + :name name of the function to be applied + """ + # _path = _args['path'] #os.sep.join([_args['root'],'plugin']) + + if os.path.isdir(_path): + files = os.listdir(_path) + if files : + files = [name for name in files if name.endswith('.py') and name == _filename] + if files: + _path = os.sep.join([_path,files[0]]) + else: + return None + else: + return None + #-- We have a file ... + # _name = _args['name'] + spec = importlib.util.spec_from_file_location(_filename, _path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return getattr(module,_name) if hasattr(module,_name) else None + + diff --git a/cms/engine/project/__init__.py b/cms/engine/project/__init__.py new file mode 100644 index 0000000..a24339f --- /dev/null +++ b/cms/engine/project/__init__.py @@ -0,0 +1,154 @@ +# from cms import _system, _header,_layout +import base64 +import os +import json +import meta +from cms.engine import themes +# 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['footer']] +# else: +# _info['layout']['footer'] = [{'text':'Powered by QCMS'}] + +# return _info +def make_folder (projectFolder, webroot): + """ + This function creates project folders, inside the project folder is the web root folder + """ + + if not os.path.exists(projectFolder): + os.makedirs(projectFolder) + _path = os.sep.join([projectFolder,webroot]) + folders = ['_assets',os.sep.join(['_assets','images']), os.sep.join(['_assets','themes']), '_plugins'] + for folder in folders : + _projectPath = os.sep.join([_path,folder]) + if not os.path.exists(_projectPath) : + os.makedirs(_projectPath) +def _ilogo (_path): + """ + This function creates a default logo in a designated folder, after the project folder has been created + :_path project path + """ + _image = """data:image/png;base64,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""" + _image1 = """data:image/png;base64,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""" + _image2 = """data:image/png;base64,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""" + _index = 0 + for _image in [_image1,_image2] : + _,_image= _image.split(',',1) + _logoPath = os.sep.join([_path,'_assets','images','logo.png']) + if _index > 0 : + _logoPath = _logoPath.replace('.png',f'-{_index}.png') + f = open(_logoPath,'wb') + f.write(base64.b64decode(_image)) + f.close() + _index += 1 + +def _index (_path,root): + """ + Creating a default index.html for the site given the project root location + """ + _html = f""" +
+
Thank you for considering QCMS
+
version {meta.__version__} by {meta.__author__}, {meta.__email__}
+
+

+ +

+ +
+
+
+ +
+ QCMS is powered by Python/Flask +
    + Small footprint & can work as either a portal or a standalone application +
+
    + Simple paradigm that leverages folder structure to generate a site +
+
+ +
+
+ QCMS has built-in support for industry standard frameworks +
+ + +
+
    + fontawesome +
    jQuery +
    Apexcharts +
+
    + As a python/flask enabled framework, there can also be support for the wide range of libraries for various AI/ML projects +
+ +
+
+

+

Learn more about QCMS and at {themes.URL}
+

+ """ + _indexfile = os.sep.join([_path,'index.html']) + if os.path.exists(_indexfile): + # + # In case a project needs to be upgraded ... + f = open(_indexfile,'w') + f.write(_html) + f.close() +def _itheme(_path,_root,_name='default'): + _data = themes.Get(_name)[_name] + _themefolder = os.sep.join([_path,_root,'_assets','themes',_name]) + if not os.path.exists(_themefolder) : + os.makedirs(_themefolder) + + for _name in _data : + f = open(os.sep.join([_themefolder,_name+'.css']),'w') + f.write(_data[_name]) + f.close() + pass +def make (**_args) : + """ + This function create a project folder and within the folder are all the elements needed + """ + _config = _args['config'] + _config['plugins'] = {} + _folder = _args['folder'] + _root = _config['layout']['root'] #-- web root folder + make_folder(_folder,_root) + f = open(os.sep.join([_folder,'qcms-manifest.json']),'w') + f.write( json.dumps(_config)) + f.close() + + _ilogo(os.sep.join([_folder,_root])) + _index(os.sep.join([_folder,_root]),_root) + _itheme(_folder,_root) + \ No newline at end of file diff --git a/cms/engine/themes/__init__.py b/cms/engine/themes/__init__.py new file mode 100644 index 0000000..d3d00b7 --- /dev/null +++ b/cms/engine/themes/__init__.py @@ -0,0 +1,44 @@ +""" +This class implements the base infrastructure to handle themes, the class must leverage +default themes will be stored in layout.root._assets.themes, The expected files in a theme are the following: + - borders + - buttons + - layout + - menu + - header +If the following are not available then we should load the default one. +This would avoid crashes but would come at the expense of a lack of consistent visual layout (alas) +""" +import requests +import os +URL = os.environ['QCMS_HOME_URL'] if 'QCMS_HOME_URL' in os.environ else 'https://dev.the-phi.com/qcms' +def current (_system) : + return _system['theme'] +def List (_url = URL) : + """ + calling qcms to list the available URL + """ + try: + _url = '/'.join([_url,'api','themes','List']) + return requests.get(_url).json() + except Exception as e: + + pass + return [] +def Get(theme,_url= URL) : + """ + This function retrieves a particular theme from a remote source + The name should be in the list provided by the above function + """ + try: + _url = '/'.join([_url,'api','themes','Get']) +f'?theme={theme}' + return requests.get(_url).json() + except Exception as e: + pass + return {} +def installed (path): + return os.listdir(os.sep.join([path,'_assets','themes'])) + +def Set(theme,_system) : + _system['theme'] = theme + return _system diff --git a/cms/static/css/dialog.css b/cms/static/css/dialog.css new file mode 100644 index 0000000..7ec2adf --- /dev/null +++ b/cms/static/css/dialog.css @@ -0,0 +1,10 @@ + +.dialog-title { + background-color:#FF6500;color:#FFFFFF; + text-transform:capitalize; font-weight:bold; align-items:center;display:grid; grid-template-columns:auto 32px; +} +.dialog-button { + display:grid; + grid-template-columns: auto 115px; + gap:4px; +} diff --git a/cms/static/css/source-code.css b/cms/static/css/source-code.css new file mode 100644 index 0000000..e46f72c --- /dev/null +++ b/cms/static/css/source-code.css @@ -0,0 +1,18 @@ +.source-code { + background-color: #000000; COLOR:#ffffff; + font-family: 'Courier New', Courier, monospace; + padding:8px; + padding-left:10px; + text-wrap: wrap; + width:calc(100% - 40px); + border-left:8px solid #CAD5E0; margin-left:10px; font-weight: bold; + font-size:14px; +} + +.editor { + background-color:#f3f3f3; + color:#000000; +} + +.editor .keyword {color: #4682b4; font-weight:bold} +.code-comment { font-style: italic; color:gray; font-size:13px;} \ No newline at end of file From f231e453ca634871cb7d86ce9154d80d85f0cac7 Mon Sep 17 00:00:00 2001 From: Steve Nyemba Date: Sun, 21 Jul 2024 17:39:29 -0500 Subject: [PATCH 02/18] theme engine, and cli updated, refactored --- bin/qcms | 388 +++++++++++++++---------------- cms/__init__.py | 238 +++++-------------- cms/cloud.py | 12 +- cms/disk.py | 24 +- cms/index.py | 33 +-- cms/plugins.py | 16 -- cms/static/css/themes/oss.css | 2 + cms/static/css/themes/resume.css | 3 +- cms/templates/header.html | 17 +- cms/templates/index.html | 23 +- meta/__init__.py | 15 +- 11 files changed, 322 insertions(+), 449 deletions(-) delete mode 100644 cms/plugins.py diff --git a/bin/qcms b/bin/qcms index d6272be..bd11e81 100755 --- a/bin/qcms +++ b/bin/qcms @@ -5,30 +5,33 @@ import sys import json import importlib import importlib.util -from git.repo.base import Repo +# from git.repo.base import Repo import typer from typing_extensions import Annotated from typing import Optional +from typing import Tuple + + import meta import uuid from termcolor import colored import base64 import io +import cms from cms import index -start = index.start +from cms.engine.config.structure import Layout, System +from cms.engine import project, config, themes, plugins +import pandas as pd +import requests -__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 --version --title --subtitle <subtitle> -""" +start = index.start +__doc__ = f""" +Built and designed by Steve L. Nyemba, steve@the-phi.com +version {meta.__version__} + +{meta.__license__}""" PASSED = ' '.join(['[',colored(u'\u2713', 'green'),']']) FAILED= ' '.join(['[',colored(u'\u2717','red'),']']) @@ -40,94 +43,6 @@ INVALID_FOLDER = """ # 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(): @@ -135,23 +50,23 @@ def _info(): This function returns metadata information about this program, no parameters are needed """ print () - print (meta.__name__,meta.__version__) + print ('QCMS',meta.__version__) print (meta.__author__,meta.__email__) print () -@cli.command(name='set-app') +@cli.command(name='setup') # 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 + Setup application access i.e port, debug, and/or context + These parameters are the same used in any flask application - :context alias or path for applications living behind proxy server """ global INVALID_FOLDER - _config = _get_config() + _config = config.get() if _config : _app = _config['system']['app'] _app['host'] = host @@ -159,21 +74,18 @@ def set_app (host:Annotated[str,typer.Argument(help="bind host IP address")]="0. _app['debug'] = debug _config['system']['context'] = context _config['app'] = _app - write_config(_config) + config.write(_config) _msg = f"""{PASSED} Successful update, good job ! """ else: _msg = INVALID_FOLDER print (_msg) -@cli.command(name='set-cloud') +@cli.command(name='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 + Setup qcms to generate a site from files on nextcloud + The path must refrence auth-file (data-transport) """ if os.path.exists(path): f = open (path) @@ -182,13 +94,12 @@ def set_cloud(path:Annotated[str,typer.Argument(help="path of the auth-file for url = _auth['url'] if os.path.exists('qcms-manifest.json') : - _config = _get_config() + _config = config.get() _config['system']['source'] = path #{'id':'cloud','auth':{'url':url,'uid':uid,'token':token}} - write_config(_config) + config.write(_config) title = _config['layout']['header']['title'] - _msg = f""" - Successfully update, good job! - {url} + _msg = f"""{PASSED} Successfully update, good job! + {url} """ else: _msg = INVALID_FOLDER @@ -197,13 +108,15 @@ def set_cloud(path:Annotated[str,typer.Argument(help="path of the auth-file for else: _msg = INVALID_FOLDER print (_msg) -@cli.command(name='set-key') +@cli.command(name='secure') # def set_key(path): -def set_key( +def secure( 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. + Create a security key and set it in the manifest (and store in on disk). + The secure key allows for administrative control of the application/portal + """ if not os.path.exists(keyfile): @@ -211,11 +124,11 @@ def set_key( f.write(str(uuid.uuid4())) f.close() # - _config = _get_config(manifest) + _config = config.get(manifest) if 'source' not in _config['system']: _config['system']['source'] = {'id':'disk'} _config['system']['source']['key'] = keyfile - write_config(_config,manifest) + config.write(_config,manifest) _msg = f"""{PASSED} A key was generated and written to {keyfile} use this key in header to enable reload of the site ...` """ @@ -252,103 +165,186 @@ def load(**_args): return getattr(module,_name) if hasattr(module,_name) else None +@cli.command(name='plugins') +def plug_info (manifest:Annotated[str,typer.Argument(help="path to manifest file")], + show:bool=typer.Option(default=False,help="list plugins loaded"), + add: Annotated[Optional[bool],typer.Option("--register/--unregister",help="add/remove a plugin to manifest use with --pointer option")] = None, + pointer:str=typer.Option(default=None,help="pointer is structured as 'filename.function'") + ) : + """ + Manage plugins list loaded plugins, + """ + _config = config.get(manifest) + _root = os.sep.join(manifest.split(os.sep)[:-1] + [_config['layout']['root'],'_plugins']) + if os.path.exists(_root) : + # files = os.listdir(_root) + _msg = f"""{FAILED} no operation was specified, please use --help option""" + # if 'plugins' in _config : + _plugins = _config['plugins'] if 'plugins' in _config else {} + if show : + if not _plugins : + _msg = f"""{FAILED} no plugins are loaded\n\t{manifest}""" + else: + _data = plugins.stats(_plugins) + print (_data) + _msg = f"""{PASSED} found a total of {_data.loaded.sum()} plugins loaded from {_data.shape[0]} file(s)\n\t{_root}""" + + if add in [True,False] and pointer : + file,fnName = pointer.split('.') + _fnpointer = plugins.load(_root,file+'.py',fnName) + if _fnpointer and add: + if file not in _plugins : + _plugins[file] = [] + + if fnName not in _plugins[file] : + _plugins[file].append(fnName) + _msg = f"""{PASSED} registered {pointer}, use the --show option to list loaded plugins\n{manifest} """ + else: + _msg = f"""{FAILED} could not register {pointer}, it already exists\n\t{manifest} """ + elif add is False and file in _plugins: + _plugins[file] = [_name.strip() for _name in _plugins[file] if _name.strip() != fnName.strip() ] + + _msg = f"""{PASSED} unregistered {pointer}, use the --show option to list loaded plugins\n{manifest} """ + + # + # We need to write this down !! + if add in [True,False] : + + _config['plugins'] = _plugins + config.write(_config,manifest) + # else: + # _msg = f"""{FAILED} no plugins are loaded\n\t{manifest}""" + print() + print(_msg) + else: + _msg = f"""{FAILED} no plugin folder found """ + pass + + @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)")], + # 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' + footer:str=typer.Option(default="Powered By QCMS",help="text to be placed as footer"), #Annotated[str,typer.Argument(help="text on the footer of the main page")]='Quick Content Management System', + port:int=typer.Option(default=8084, help="port to be used for the app"), + context:str=typer.Option(default="", help="application context if using a proxy server"), + version:str=typer.Option(default=meta.__version__,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) + 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']) - + # Build the configuration for the project + if not os.path.exists(folder) : + root = 'www/html' + _system = System() + _layout = Layout() + _layout = _layout.build(root=root, footer = [{"text":term} for term in footer.split(',')],header={'title':title,'subtitle':subtitle} ) + + _system = System() + _system = _system.build(version=version,port=port, context=context) + _config = dict(_system,**_layout) + print (f"{PASSED} Built Configuration Object") + # + # Setup Project on disk + # + project.make(folder=folder,config=_config) + print (f"""{PASSED} created project at {folder} """) + else: + print () + print (f"""{FAILED} Could not create project {folder} + The folder is not empty + """) - 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 +def reload ( + path:Annotated[str,typer.Argument(help="")], + port:int=typer.Option(default=None,help="port of the host to call") + ) : + """ + Reload a site/portal given the manifest ... + """ + _config = config.get(path) + if 'key' in _config['system']['source'] : + f = open(_config['system']['source']['key']) + key = f.read() + f.close() + _port = port if port else _config['system']['app']['port'] + url = f"http://localhost:{_port}/reload" + resp = requests.post(url, headers={"key":key}) + if resp.status_code == 200 : + _msg = f"""{PASSED} successfully reloaded {url}""" + else: + _msg = f"""{FAILED} failed to reload, status code {resp.status_code}\n{url} + """ + else: + _msg = f"""{FAILED} no secure key found in manifest""" + print (_msg) @cli.command(name="bootup") def bootup ( - path:Annotated[str,typer.Argument(help="path of the manifest file")]='qcms-manifest.json', + manifest: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(): + index.start(manifest,port) +@cli.command(name='theme') +def handle_theme ( + manifest:Annotated[str,typer.Argument(help="path of the manifest file")], + show:bool = typer.Option(default=False,help="print list of themes available"), + name:str = typer.Option(default='default',help='name of the theme to apply') + ) : """ + This function will show the list available themes and can also set a theme in a manifest + """ + _config = config.get(manifest) + _root = os.sep.join( manifest.split(os.sep)[:-1]+[_config['layout']['root']]) + + if show : + _df = pd.DataFrame({"available":themes.List()}) + if _df.shape[0] > 0 : + values = themes.installed(_root) + values = values + np.repeat(f"""{FAILED}""", _df.shape[0] - len(values)).tolist() + _df['installed'] = values + else: + _df = f"""{FAILED} No themes were found in registry,\ncurl {themes.URL}/api/themes/List (QCMS_HOST_URL should be set)""" + print (_df) + if name : + # we need to install the theme, i.e download it and update the configuration + # + try: + _object = themes.Get(name) + path = os.sep.join([_root,'_assets','themes',name]) + if not os.path.exists(path) : + os.makedirs(path) + _object = _object[name] #-- formatting (...) + for _filename in _object : + css = _object[_filename] + f = open(os.sep.join([path,_filename+'.css']),'w') + f.write(css) + f.close() + + # + # Let us update the configuration file ... + # + _config['system']['theme'] = name + config.write(_config,manifest) + _msg = f"""{PASSED} successfully downloaded {name} \n{PASSED} updated manifest {manifest} + """ + except Exception as e: + _msg = f"""{FAILED} operation failed "{str(e)}" + """ + + pass + print (_msg) + global SYS_ARGS if __name__ == '__main__': cli() diff --git a/cms/__init__.py b/cms/__init__.py index 3f98a0b..5fe6631 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -6,192 +6,62 @@ import copy from jinja2 import Environment, BaseLoader, FileSystemLoader import importlib import importlib.util -# 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 - -# @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() +# 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() -# #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) : -# # 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 +# 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']) -# 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: -# # 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 - +# 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 + \ No newline at end of file diff --git a/cms/cloud.py b/cms/cloud.py index 8a7b625..8b96573 100644 --- a/cms/cloud.py +++ b/cms/cloud.py @@ -128,13 +128,15 @@ def html (uri,_config) : _html = _handler.get_file_contents(uri).decode('utf-8')#.replace('.attachments.', copy.deepcopy(_link)) # print ([uri,uri[-2:] ,uri[-2:] in ['md','MD','markdown']]) _handler.logout() + # if uri.endswith('.md'): - 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)) + if f"{_root}{os.sep}" in _html : + 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 + return markdown(_html).replace(""",'"').replace("<","<").replace(">",">") if uri[-2:] in ['md','MD','Md','mD'] else _html # def update (_config): # """ # This function updates the configuration provided by loading default plugins diff --git a/cms/disk.py b/cms/disk.py index 4426be5..8a3b8f6 100644 --- a/cms/disk.py +++ b/cms/disk.py @@ -6,7 +6,7 @@ import importlib import importlib.util import copy from mistune import markdown - +import re def folders (_path,_config): """ @@ -86,13 +86,22 @@ def read (**_args): _uri = request.args['uri'] # if 'location' in _layout : # _uri = os.sep.join([_layout['location'],_uri]) _uri = _realpath(_uri, _args['config']) + _mimeType = 'text/plain' if os.path.exists(_uri): f = open(_uri,mode='rb') _stream = f.read() f.close() - - return _stream - return None + # + # Inferring the type of the data to be returned + _mimeType = 'application/octet-stream' + _extension = _uri.split('.')[-1] + if _extension in ['css','js','csv'] : + _mimeType = f'text/{_extension}' + elif _extension in ['png','jpg','jpeg'] : + _mimeType = f'image/{_extension}' + + return _stream, _mimeType + return None,_mimeType def exists(**_args): _path = _realpath(_args['uri'],_args['config']) @@ -111,9 +120,10 @@ def html(_uri,_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 + if f"{_layout['root']}{os.sep}" in _html : + _html = _html.replace(f"{_layout['root']}",_api) + _html = markdown(_html).replace(""",'"').replace("<","<").replace(">",">") if _uri[-2:] in ['md','MD','Md','mD'] else _html + return _html def plugins (**_args): """ diff --git a/cms/index.py b/cms/index.py index c090e79..7e002b6 100644 --- a/cms/index.py +++ b/cms/index.py @@ -77,38 +77,21 @@ def robots_txt(): 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() + global _route + 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') + uri = os.sep.join([_config['layout']['root'], _config['layout']['index']]) _index_page = "index.html" + print ([_route]) _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 @@ -139,7 +122,7 @@ def _delegate_call(app,module,name): return _delegate(_handler,module,name) @_app.route('/api/<module>/<name>') -def _getproxy(module,name) : +def _api(module,name) : """ This endpoint will load a module and make a function call :_module entry specified in plugins of the configuration @@ -171,13 +154,15 @@ def _delegate(_handler,module,name): # else: # _data = pointer() - _data = pointer(request=request,config=_handler.config()) + _data,_mimeType = pointer(request=request,config=_handler.config()) + + _mimeType = 'application/octet-stream' if not _mimeType else _mimeType 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 + return _data,_code,{'Content-Type':_mimeType} @_app.route("/api/<module>/<name>" , methods=['POST']) def _post (module,name): # global _config diff --git a/cms/plugins.py b/cms/plugins.py deleted file mode 100644 index e34862b..0000000 --- a/cms/plugins.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -These are a few default plugins that will be exported and made available to the calling code -The purpose of plugins is to perform data processing -""" -import json - - - -def copyright (_args) : - return {"author":"Steve L. Nyemba","email":"steve@the-phi.com","organization":"The Phi Technology","license":"MIT", "site":"https://dev.the-phi.com/git/cloud/qcms"} - -def log (_args): - """ - perform logging against duckdb - """ - pass \ No newline at end of file diff --git a/cms/static/css/themes/oss.css b/cms/static/css/themes/oss.css index 8818714..839d7d6 100644 --- a/cms/static/css/themes/oss.css +++ b/cms/static/css/themes/oss.css @@ -7,6 +7,8 @@ margin-left:10%; margin-right:10%; gap:4px; + + } .main .header { height:64px; display:grid; diff --git a/cms/static/css/themes/resume.css b/cms/static/css/themes/resume.css index d626468..3a06a0e 100644 --- a/cms/static/css/themes/resume.css +++ b/cms/static/css/themes/resume.css @@ -8,10 +8,11 @@ .main .header { display:grid; grid-template-columns: 64px auto; gap:4px; + align-items: center; } .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} +.main .menu {margin-left:200px} .pane {width:50%; height:100px; background-color: pink;} \ No newline at end of file diff --git a/cms/templates/header.html b/cms/templates/header.html index 128e314..26428a6 100644 --- a/cms/templates/header.html +++ b/cms/templates/header.html @@ -1,11 +1,10 @@ -{% if layout.header.logo == True %} - <div class="icon"> - <img src="{{system.icon}}"> - </div> + +<div class="icon"> + <img src="{{system.icon}}"> +</div> -{% endif %} - <div> - <div class="title">{{layout.header.title}}</div> - <div class="subtitle">{{layout.header.subtitle}}</div> - </div> \ No newline at end of file +<div> + <div class="title">{{layout.header.title}}</div> + <div class="subtitle">{{layout.header.subtitle}}</div> +</div> \ No newline at end of file diff --git a/cms/templates/index.html b/cms/templates/index.html index 2486f08..a99be3d 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -23,14 +23,27 @@ Vanderbilt University Medical Center <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="keywords" content="quick cms, cms, python, flask, qcms"> <meta name="robots" content="/, follow, max-snippet:-1, max-image-preview:large"> - <link href="{{system.context}}/static/css/default.css" rel="stylesheet" type="text/css"> - <link href="{{system.context}}/static/css/menu.css" rel="stylesheet" type="text/css"> - <link href="{{system.context}}/static/css/border.css" rel="stylesheet" type="text/css"> + <!-- <link href="{{system.context}}/static/css/default.css" rel="stylesheet" type="text/css"> --> + <!-- <link href="{{system.context}}/static/css/menu.css" rel="stylesheet" type="text/css"> --> + <!-- <link href="{{system.context}}/static/css/border.css" rel="stylesheet" type="text/css"> --> <!-- <link href="{{system.context}}/static/css/animation/_ocean.css" rel="stylesheet" type="text/css"> --> - <link href="{{system.context}}/static/css/themes/{{system.theme}}" rel="stylesheet" type="text/css"> - <link href="{{system.context}}/static/css/icons.css" rel="stylesheet" type="text/css"> + <!-- <link href="{{system.context}}/static/css/themes/{{system.theme}}" rel="stylesheet" type="text/css"> --> + <!-- <link href="{{system.context}}/static/css/icons.css" rel="stylesheet" type="text/css"> --> + <link href="{{system.context}}/static/css/icons.css" rel="stylesheet" type="text/css"> + <link href="{{system.context}}/static/css/source-code.css" rel="stylesheet" type="text/css"> + <link href="{{system.context}}/static/css/search.css" rel="stylesheet" type="text/css"> + <link href="{{system.context}}/static/css/dialog.css" rel="stylesheet" type="text/css"> + + <!-- applying themes as can --> + <link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/layout.css" rel="stylesheet" type="text/css"> + <link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/header.css" rel="stylesheet" type="text/css"> + <link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/menu.css" rel="stylesheet" type="text/css"> + <link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/borders.css" rel="stylesheet" type="text/css"> + <link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/footer.css" rel="stylesheet" type="text/css"> + <link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/pane.css" rel="stylesheet" type="text/css"> + <script src="{{system.context}}/static/js/jx/dom.js"></script> <script src="{{system.context}}/static/js/jx/utils.js"></script> <script src="{{system.context}}/static/js/jx/rpc.js"></script> diff --git a/meta/__init__.py b/meta/__init__.py index 1a6b54e..e6d5dc9 100644 --- a/meta/__init__.py +++ b/meta/__init__.py @@ -1,4 +1,15 @@ -__author__ = "Steve L. Nyemba<steve@the-phi.com>" -__version__= "2.0" +__author__ = "Steve L. Nyemba" +__version__= "2.1" +__email__ = "steve@the-phi.com" __license__=""" +Copyright 2010 - 2024, Steve L. Nyemba, Vanderbilt University Medical Center + + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ From 14b4da788febc8dbc8d3b3a0baed6c168d1bcac2 Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Mon, 22 Jul 2024 15:58:01 -0500 Subject: [PATCH 03/18] bug fix: handling context with dialogs --- bin/qcms | 4 +++- cms/static/js/menu.js | 4 ++-- cms/templates/menu.html | 2 +- meta/__init__.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/bin/qcms b/bin/qcms index bd11e81..2cfc100 100755 --- a/bin/qcms +++ b/bin/qcms @@ -311,12 +311,14 @@ def handle_theme ( _df = pd.DataFrame({"available":themes.List()}) if _df.shape[0] > 0 : values = themes.installed(_root) + values.sort() values = values + np.repeat(f"""{FAILED}""", _df.shape[0] - len(values)).tolist() + values.sort() _df['installed'] = values else: _df = f"""{FAILED} No themes were found in registry,\ncurl {themes.URL}/api/themes/List (QCMS_HOST_URL should be set)""" print (_df) - if name : + if name and not show: # we need to install the theme, i.e download it and update the configuration # try: diff --git a/cms/static/js/menu.js b/cms/static/js/menu.js index dfe809c..55c1b46 100644 --- a/cms/static/js/menu.js +++ b/cms/static/js/menu.js @@ -52,7 +52,7 @@ menu.apply = function (uri,id,pid,_context){ } -menu.apply_link =function(_args){ +menu.apply_link =function(_args,_context){ // // type: // redirect open new window @@ -75,7 +75,7 @@ menu.apply_link =function(_args){ http.setHeader('uri',_args.uri) http.setHeader('dom',(_args.title)?_args.title:'dialog') // http.setHeader('dom',_args.text) - http.get('/dialog',function(x){ + http.get(_context+'/dialog',function(x){ jx.modal.show({html:x.responseText,id:'dialog'}) console.log([$('.jxmodal')]) diff --git a/cms/templates/menu.html b/cms/templates/menu.html index 9cf320d..40b5ccc 100644 --- a/cms/templates/menu.html +++ b/cms/templates/menu.html @@ -29,7 +29,7 @@ <div class="active" onclick="menu.apply('{{_item.uri}}','{{_item.text}}','{{_name}}','{{system.context}}')"> {% else %} <!-- working on links/widgets --> - <div class="active" onclick='menu.apply_link({{_item|tojson}})'> + <div class="active" onclick="menu.apply_link({{_item|tojson}},'system.context')"> {% endif %} <i class="fa-solid fa-chevron-right" style="margin-right:4px"></i> {{_item.text.replace('-',' ').replace('_',' ')}} diff --git a/meta/__init__.py b/meta/__init__.py index e6d5dc9..97ec72e 100644 --- a/meta/__init__.py +++ b/meta/__init__.py @@ -1,5 +1,5 @@ __author__ = "Steve L. Nyemba" -__version__= "2.1" +__version__= "2.1.2" __email__ = "steve@the-phi.com" __license__=""" Copyright 2010 - 2024, Steve L. Nyemba, Vanderbilt University Medical Center From b58abb884795ee890942797cbd69bb72e922fb6d Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Mon, 22 Jul 2024 16:02:34 -0500 Subject: [PATCH 04/18] bug fix: typo --- cms/templates/menu.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/menu.html b/cms/templates/menu.html index 40b5ccc..fb58d4c 100644 --- a/cms/templates/menu.html +++ b/cms/templates/menu.html @@ -29,7 +29,7 @@ <div class="active" onclick="menu.apply('{{_item.uri}}','{{_item.text}}','{{_name}}','{{system.context}}')"> {% else %} <!-- working on links/widgets --> - <div class="active" onclick="menu.apply_link({{_item|tojson}},'system.context')"> + <div class="active" onclick="menu.apply_link({{_item|tojson}},'{{system.context}}')"> {% endif %} <i class="fa-solid fa-chevron-right" style="margin-right:4px"></i> {{_item.text.replace('-',' ').replace('_',' ')}} From 5e5ccddfb3067511f5008062e2bca06aec61d8b4 Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Mon, 22 Jul 2024 16:03:11 -0500 Subject: [PATCH 05/18] bug fix: typo --- meta/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meta/__init__.py b/meta/__init__.py index 97ec72e..0981385 100644 --- a/meta/__init__.py +++ b/meta/__init__.py @@ -1,5 +1,5 @@ __author__ = "Steve L. Nyemba" -__version__= "2.1.2" +__version__= "2.1.3" __email__ = "steve@the-phi.com" __license__=""" Copyright 2010 - 2024, Steve L. Nyemba, Vanderbilt University Medical Center From d5ecc894793e881a95bf018c197480f00263b74f Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Mon, 22 Jul 2024 16:16:24 -0500 Subject: [PATCH 06/18] bug fix: context handling with dialogs --- cms/templates/menu.html | 2 +- meta/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/templates/menu.html b/cms/templates/menu.html index fb58d4c..085eadb 100644 --- a/cms/templates/menu.html +++ b/cms/templates/menu.html @@ -29,7 +29,7 @@ <div class="active" onclick="menu.apply('{{_item.uri}}','{{_item.text}}','{{_name}}','{{system.context}}')"> {% else %} <!-- working on links/widgets --> - <div class="active" onclick="menu.apply_link({{_item|tojson}},'{{system.context}}')"> + <div class="active" onclick='menu.apply_link({{_item|tojson}},"{{system.context}}")'> {% endif %} <i class="fa-solid fa-chevron-right" style="margin-right:4px"></i> {{_item.text.replace('-',' ').replace('_',' ')}} diff --git a/meta/__init__.py b/meta/__init__.py index 0981385..61074d7 100644 --- a/meta/__init__.py +++ b/meta/__init__.py @@ -1,5 +1,5 @@ __author__ = "Steve L. Nyemba" -__version__= "2.1.3" +__version__= "2.1.4" __email__ = "steve@the-phi.com" __license__=""" Copyright 2010 - 2024, Steve L. Nyemba, Vanderbilt University Medical Center From b468e1ad0a99abc80a4972ddfcf81a2dc0750802 Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Fri, 9 Aug 2024 15:02:43 -0500 Subject: [PATCH 07/18] bug fixes --- cms/disk.py | 4 +++- cms/engine/project/__init__.py | 3 ++- cms/index.py | 30 ++++++++++++++++++++++++------ cms/templates/index.html | 2 +- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/cms/disk.py b/cms/disk.py index 8a3b8f6..3d31f76 100644 --- a/cms/disk.py +++ b/cms/disk.py @@ -5,6 +5,7 @@ import os import importlib import importlib.util import copy +import mistune from mistune import markdown import re @@ -122,7 +123,8 @@ def html(_uri,_config) : _api = os.sep.join([f'{_context}/api/disk/read?uri=',_layout['root']]) if f"{_layout['root']}{os.sep}" in _html : _html = _html.replace(f"{_layout['root']}",_api) - _html = markdown(_html).replace(""",'"').replace("<","<").replace(">",">") if _uri[-2:] in ['md','MD','Md','mD'] else _html + _html = mistune.html(_html).replace(""",'"').replace("<","<").replace(">",">") if _uri[-2:] in ['md','MD','Md','mD'] else _html + return _html def plugins (**_args): diff --git a/cms/engine/project/__init__.py b/cms/engine/project/__init__.py index a24339f..7b1e6ef 100644 --- a/cms/engine/project/__init__.py +++ b/cms/engine/project/__init__.py @@ -118,7 +118,7 @@ def _index (_path,root): </p> """ _indexfile = os.sep.join([_path,'index.html']) - if os.path.exists(_indexfile): + if not os.path.exists(_indexfile) and os.path.exists(_path): # # In case a project needs to be upgraded ... f = open(_indexfile,'w') @@ -149,6 +149,7 @@ def make (**_args) : f.close() _ilogo(os.sep.join([_folder,_root])) + print ([_folder,_root]) _index(os.sep.join([_folder,_root]),_root) _itheme(_folder,_root) \ No newline at end of file diff --git a/cms/index.py b/cms/index.py index 7e002b6..5fbe446 100644 --- a/cms/index.py +++ b/cms/index.py @@ -177,23 +177,41 @@ def _version (): _handler = _route.get() global _config return _handler.system()['version'] -@_app.route('/reload',methods=['POST']) -def reload(): +@_app.route("/reload/<key>") +def _reload(key) : 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 : + print ([key,_systemKey,_systemKey == key]) + if key and _systemKey and _systemKey == key : _handler.reload() return "",200 pass - return "",403 + return "",403 + +@_app.route('/reload',methods=['POST']) +def reload(): + # global _route + + # _handler = _route.get_main() + # _system = _handler.system() + _key = request.headers['key'] if 'key' in request.headers else None + return _reload(_key) + # if not 'source' in _system : + # _systemKey = None + # elif 'key' in _system['source'] and _system['source']['key']: + # _systemKey = _system['source']['key'] + # print ([_key,_systemKey,_systemKey == _key]) + # if _key and _systemKey and _systemKey == _key : + # _handler.reload() + # return "",200 + # pass + # return "",403 @_app.route('/page',methods=['POST']) def cms_page(): """ diff --git a/cms/templates/index.html b/cms/templates/index.html index a99be3d..dc9a2eb 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -69,7 +69,7 @@ Vanderbilt University Medical Center </script> <body> - <div class="main"> + <div class="main {{system.theme}}"> <div id="header" class="header" onclick="window.location.href='{{system.context}}/'" style="cursor:pointer"> {%include "header.html" %} </div> From b283d26717b9304e0263a5df565ab2da1738fa16 Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Wed, 21 Aug 2024 15:19:19 -0500 Subject: [PATCH 08/18] layout fixes ... --- cms/static/css/menu.css | 34 ++++++++++++++++++++++++++++++++++ cms/static/css/search.css | 7 +++++++ 2 files changed, 41 insertions(+) diff --git a/cms/static/css/menu.css b/cms/static/css/menu.css index a084c5c..af77619 100644 --- a/cms/static/css/menu.css +++ b/cms/static/css/menu.css @@ -1,3 +1,8 @@ +/** +* This file implements styling for menus i.e we have +* 1. Traditional menus and +* 2. Tabbed menus +*/ .menu { padding:8px; border:1px solid #CAD5E0 ; @@ -44,4 +49,33 @@ display:block; height:auto; +} + +/** +* This section shows how we use css to implement tabs i.e we will be using radio boxes and labels +* +*/ +.tabs {display:grid; grid-template-columns: repeat(auto-fit,209px); gap:0px; align-content:center; + /* background-color: #f3f3f3; */ + padding-top:4px; + padding-left:4px; + padding-right:4px; +} +.tabs input[type=radio] {display:none; } +.tabs input[type=radio] + label { font-weight:lighter; + border:1px solid transparent; + border-bottom-color: #CAD5E0; + background-color: #f3f3f3; + padding:8px; + padding-right:10px; padding-left:10px; + + cursor:pointer +} +.tabs input[type=radio]:checked +label { + background-color: #ffffff; + border-top-right-radius: 8px; + border-top-left-radius: 8px; + font-weight:bold; + border-color: #CAD5E0; + border-bottom-color: #FFFFFF; } \ No newline at end of file diff --git a/cms/static/css/search.css b/cms/static/css/search.css index e473425..835bffb 100644 --- a/cms/static/css/search.css +++ b/cms/static/css/search.css @@ -2,6 +2,13 @@ /** * components: search */ + +/* .search-box { + display:grid; + grid-template-columns: auto 64px; gap:4px; + +} */ + .search .frame .suggestion-frame { width:98%; overflow: hidden; From 4e2430fc2185d24b17482cd01cebf7e5f3c751d1 Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Mon, 26 Aug 2024 10:12:37 -0500 Subject: [PATCH 09/18] routes withe depths --- cms/disk.py | 4 +- cms/engine/__init__.py | 679 +++++++++++++++++++------------------- cms/engine/basic.py | 40 ++- cms/index.py | 216 +++++++----- cms/static/js/bootup.js | 29 +- cms/templates/header.html | 1 - cms/templates/index.html | 32 +- cms/templates/menu.html | 2 +- 8 files changed, 531 insertions(+), 472 deletions(-) diff --git a/cms/disk.py b/cms/disk.py index 3d31f76..da2be29 100644 --- a/cms/disk.py +++ b/cms/disk.py @@ -113,7 +113,9 @@ def exists(**_args): def html(_uri,_config) : # _html = (open(uri)).read() _path = _realpath(_uri,_config) - _context = _config['system']['context'] + _context = str(_config['system']['context']) + # if '/' in _context : + # _context = _context.split('/')[-1] _html = ( open(_path)).read() _layout = _config['layout'] if 'location' in _layout : diff --git a/cms/engine/__init__.py b/cms/engine/__init__.py index f6200cc..9d03eac 100644 --- a/cms/engine/__init__.py +++ b/cms/engine/__init__.py @@ -1,384 +1,385 @@ -import json +# import json -from genericpath import isdir -import os -import pandas as pd -import transport -import copy -from jinja2 import Environment, BaseLoader, FileSystemLoader -import importlib -import importlib.util +# from genericpath import isdir +# import os +# import pandas as pd +# import transport +# import copy +# from jinja2 import Environment, BaseLoader, FileSystemLoader +# import importlib +# import importlib.util from cms import disk, cloud from . import basic -class Loader : - """ - This class is designed to exclusively load configuration from disk into an object - :path path to the configuraiton file - :location original location (caller) - """ - def __init__(self,**_args): - self._path = _args['path'] - self._original_location = None if 'location' not in _args else _args['location'] - self._location = None - self._caller = None if 'caller' not in _args else _args['caller'] - self._menu = {} - self._plugins={} - self.load() +# class Loader : +# """ +# This class is designed to exclusively load configuration from disk into an object +# :path path to the configuraiton file +# :location original location (caller) +# """ +# def __init__(self,**_args): +# self._path = _args['path'] +# self._original_location = None if 'location' not in _args else _args['location'] +# self._location = None +# self._caller = None if 'caller' not in _args else _args['caller'] +# print ([' *** ', self._caller]) +# self._menu = {} +# self._plugins={} +# self.load() - def load(self): - """ - This function will load menu (overwrite) and plugins - """ - self.init_config() - self.init_menu() - self.init_plugins() - def init_config(self): - """ - Initialize & loading configuration from disk - """ - f = open (self._path) - self._config = json.loads(f.read()) +# def load(self): +# """ +# This function will load menu (overwrite) and plugins +# """ +# self.init_config() +# self.init_menu() +# self.init_plugins() +# def init_config(self): +# """ +# Initialize & loading configuration from disk +# """ +# f = open (self._path) +# self._config = json.loads(f.read()) - if self._caller : - self._location = self._original_location.split(os.sep) # needed for plugin loading - self._location = os.sep.join(self._location[:-1]) - self._config['system']['portal'] = self._caller != None +# if self._caller : +# self._location = self._original_location.split(os.sep) # needed for plugin loading +# self._location = os.sep.join(self._location[:-1]) +# self._config['system']['portal'] = self._caller != None - # - # let's see if we have a location for a key (i.e security key) in the configuration - # - self.update_config() - # _system = self._config['system'] - # if 'source' in _system and 'key' in _system['source'] : - # _path = _system['source']['key'] - # if os.path.exists(_path): - # f = open(_path) - # _system['source']['key'] = f.read() - # f.close() - # self._system = _system - # self._config['system'] = _system - def update_config(self): - """ - We are going to update the configuration (source.key, layout.root) - """ - _system = self._config['system'] - # - # updating security-key that allows the application to update on-demand - if 'source' in _system and 'key' in _system['source'] : - _path = _system['source']['key'] - if os.path.exists(_path): - f = open(_path) - _system['source']['key'] = f.read() - f.close() - self._system = _system - self._config['system'] = _system - _layout = self._config['layout'] - # - # update root so that the app can be launched from anywhere - # This would help reduce the footprint of the app/framework - _path = os.sep.join(self._path.split(os.sep)[:-1]) - _p = 'source' not in _system - _q = 'source' in _system and _system['source']['id'] != 'cloud' - _r = os.path.exists(_layout['root']) - if not _r and (_p or _q) : - # - # If we did running this app from installed framework (this should not apply to dependent apps) - # - _root = os.sep.join([_path,_layout['root']]) - self._config['layout']['root'] = _root - self._config['layout']['root_prefix'] = _root +# # +# # let's see if we have a location for a key (i.e security key) in the configuration +# # +# self.update_config() +# # _system = self._config['system'] +# # if 'source' in _system and 'key' in _system['source'] : +# # _path = _system['source']['key'] +# # if os.path.exists(_path): +# # f = open(_path) +# # _system['source']['key'] = f.read() +# # f.close() +# # self._system = _system +# # self._config['system'] = _system +# def update_config(self): +# """ +# We are going to update the configuration (source.key, layout.root) +# """ +# _system = self._config['system'] +# # +# # updating security-key that allows the application to update on-demand +# if 'source' in _system and 'key' in _system['source'] : +# _path = _system['source']['key'] +# if os.path.exists(_path): +# f = open(_path) +# _system['source']['key'] = f.read() +# f.close() +# self._system = _system +# self._config['system'] = _system +# _layout = self._config['layout'] +# # +# # update root so that the app can be launched from anywhere +# # This would help reduce the footprint of the app/framework +# _path = os.sep.join(self._path.split(os.sep)[:-1]) +# _p = 'source' not in _system +# _q = 'source' in _system and _system['source']['id'] != 'cloud' +# _r = os.path.exists(_layout['root']) +# if not _r and (_p or _q) : +# # +# # If we did running this app from installed framework (this should not apply to dependent apps) +# # +# _root = os.sep.join([_path,_layout['root']]) +# self._config['layout']['root'] = _root +# self._config['layout']['root_prefix'] = _root - def init_menu(self): - """ - This function will read menu and sub-menu items from disk structure, - The files are loaded will - """ +# def init_menu(self): +# """ +# This function will read menu and sub-menu items from disk structure, +# The files are loaded will +# """ - _config = self._config - if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' : - _sourceHandler = cloud - else: - _sourceHandler = disk - _object = _sourceHandler.build(_config) +# _config = self._config +# if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' : +# _sourceHandler = cloud +# else: +# _sourceHandler = disk +# _object = _sourceHandler.build(_config) - # - # After building the site's menu, let us add the one from 3rd party apps - # +# # +# # After building the site's menu, let us add the one from 3rd party apps +# # - _layout = copy.deepcopy(_config['layout']) - _overwrite = _layout['overwrite'] if 'overwrite' in _layout else {} +# _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]) +# # +# # @TODO: Find a way to translate rename/replace keys of the _object (menu) here +# # +# #-- applying overwrites to the menu items +# for _name in _object : +# _submenu = _object[_name] +# _index = 0 +# for _item in _submenu : +# text = _item['text'].strip() +# if text in _overwrite : +# if 'uri' in _item and 'url' in 'url' in _overwrite[text] : +# del _item['uri'] +# _item = dict(_item,**_overwrite[text]) - if 'uri' in _item and 'type' in _item and _item['type'] != 'open': - _item['uri'] = _item['uri'].replace(_layout['root'],'') +# if 'uri' in _item and 'type' in _item and _item['type'] != 'open': +# _item['uri'] = _item['uri'].replace(_layout['root'],'') - _submenu[_index] = _item - _index += 1 - self.init_apps(_object) - self._menu = _object - self._order() +# _submenu[_index] = _item +# _index += 1 +# self.init_apps(_object) +# self._menu = _object +# self._order() - def init_apps (self,_menu): - """ - Insuring that the apps are loaded into the menu with an approriate label - """ - _system = self._config['system'] - _context = _system['context'] - if 'routes' in _system : - # _items = [] - _overwrite = {} if 'overwrite' not in self._config['layout'] else self._config['layout']['overwrite'] - for _text in _system['routes'] : - _item = _system['routes'][_text] - if 'menu' not in _item : - continue - uri = f'{_context}/{_text}' - # _items.append ({"text":_text,'uri':uri,'type':'open'}) - _label = _item['menu'] - if _label not in _menu : - _menu [_label] = [] - _menu[_label].append ({"text":_text,'uri':uri,'type':'open'}) - # _overwrite[_text] = {'text': _text.replace('-',' ').replace('_',' '),'uri':uri,'type':'open'} - # _menu['products'] = _items - # - # given that the menu items assumes redirecting to a page ... - # This is not the case - # - # self._config['overwrite'] = _overwrite - else: - pass +# def init_apps (self,_menu): +# """ +# Insuring that the apps are loaded into the menu with an approriate label +# """ +# _system = self._config['system'] +# _context = _system['context'] +# if 'routes' in _system : +# # _items = [] +# _overwrite = {} if 'overwrite' not in self._config['layout'] else self._config['layout']['overwrite'] +# for _text in _system['routes'] : +# _item = _system['routes'][_text] +# if 'menu' not in _item : +# continue +# uri = f'{_context}/{_text}' +# # _items.append ({"text":_text,'uri':uri,'type':'open'}) +# _label = _item['menu'] +# if _label not in _menu : +# _menu [_label] = [] +# _menu[_label].append ({"text":_text,'uri':uri,'type':'open'}) +# # _overwrite[_text] = {'text': _text.replace('-',' ').replace('_',' '),'uri':uri,'type':'open'} +# # _menu['products'] = _items +# # +# # given that the menu items assumes redirecting to a page ... +# # This is not the case +# # +# # self._config['overwrite'] = _overwrite +# else: +# pass - pass - def _order (self): - _config = self._config - if 'order' in _config['layout'] and 'menu' in _config['layout']['order']: - _sortedmenu = {} - _menu = self._menu - for _name in _config['layout']['order']['menu'] : - if _name in _menu : - _sortedmenu[_name] = _menu[_name] +# pass +# def _order (self): +# _config = self._config +# if 'order' in _config['layout'] and 'menu' in _config['layout']['order']: +# _sortedmenu = {} +# _menu = self._menu +# for _name in _config['layout']['order']['menu'] : +# if _name in _menu : +# _sortedmenu[_name] = _menu[_name] - _menu = _sortedmenu if _sortedmenu else _menu - # - # If there are missing items in the sorting - _missing = list(set(self._menu.keys()) - set(_sortedmenu)) - if _missing : - for _name in _missing : - _menu[_name] = self._menu[_name] - _config['layout']['menu'] = _menu #cms.components.menu(_config) - self._menu = _menu - self._config = _config - def init_plugins(self) : - """ - This function looks for plugins in the folder on disk (no cloud support) and attempts to load them - """ - _config = self._config - PATH= os.sep.join([_config['layout']['root'],'_plugins']) - if not os.path.exists(PATH) : - # - # we need to determin if there's an existing - PATH = os.sep.join(self._path.split(os.sep)[:-1]+ [PATH] ) - if not os.path.exists(PATH) and self._location and os.path.exists(self._location) : - # - # overriding the location of plugins ... - PATH = os.sep.join([self._location, _config['layout']['root'],'_plugins']) +# _menu = _sortedmenu if _sortedmenu else _menu +# # +# # If there are missing items in the sorting +# _missing = list(set(self._menu.keys()) - set(_sortedmenu)) +# if _missing : +# for _name in _missing : +# _menu[_name] = self._menu[_name] +# _config['layout']['menu'] = _menu #cms.components.menu(_config) +# self._menu = _menu +# self._config = _config +# def init_plugins(self) : +# """ +# This function looks for plugins in the folder on disk (no cloud support) and attempts to load them +# """ +# _config = self._config +# PATH= os.sep.join([_config['layout']['root'],'_plugins']) +# if not os.path.exists(PATH) : +# # +# # we need to determin if there's an existing +# PATH = os.sep.join(self._path.split(os.sep)[:-1]+ [PATH] ) +# if not os.path.exists(PATH) and self._location and os.path.exists(self._location) : +# # +# # overriding the location of plugins ... +# PATH = os.sep.join([self._location, _config['layout']['root'],'_plugins']) - _map = {} - # if not os.path.exists(PATH) : - # return _map - if 'plugins' not in _config : - _config['plugins'] = {} - _conf = _config['plugins'] +# _map = {} +# # if not os.path.exists(PATH) : +# # return _map +# if 'plugins' not in _config : +# _config['plugins'] = {} +# _conf = _config['plugins'] - for _key in _conf : +# for _key in _conf : - _path = os.sep.join([PATH,_key+".py"]) - if not os.path.exists(_path): - continue - for _name in _conf[_key] : - _pointer = self._load_plugin(path=_path,name=_name) - if _pointer : - _uri = "/".join(["api",_key,_name]) - _map[_uri] = _pointer - # - # We are adding some source specific plugins to the user-defined plugins - # This is intended to have out-of the box plugins... - # - if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' : - _plugins = cloud.plugins() - else: - _plugins = disk.plugins() - # - # If there are any plugins found, we should load them and use them +# _path = os.sep.join([PATH,_key+".py"]) +# if not os.path.exists(_path): +# continue +# for _name in _conf[_key] : +# _pointer = self._load_plugin(path=_path,name=_name) +# if _pointer : +# _uri = "/".join(["api",_key,_name]) +# _map[_uri] = _pointer +# # +# # We are adding some source specific plugins to the user-defined plugins +# # This is intended to have out-of the box plugins... +# # +# if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' : +# _plugins = cloud.plugins() +# else: +# _plugins = disk.plugins() +# # +# # If there are any plugins found, we should load them and use them - if _plugins : - _map = dict(_map,**_plugins) - else: - pass - self._plugins = _map - self._config['plugins'] = self._plugins +# if _plugins : +# _map = dict(_map,**_plugins) +# else: +# pass +# self._plugins = _map +# self._config['plugins'] = self._plugins - def _load_plugin(self,**_args): - """ - This function will load external module form a given location and return a pointer to a function in a given module - :path absolute path of the file (considered plugin) to be loaded - :name name of the function to be applied - """ - _path = _args['path'] #os.sep.join([_args['root'],'plugin']) - if os.path.isdir(_path): - files = os.listdir(_path) - if files : - files = [name for name in files if name.endswith('.py')] - if files: - _path = os.sep.join([_path,files[0]]) - else: - return None - else: - return None - #-- We have a file ... - _name = _args['name'] +# def _load_plugin(self,**_args): +# """ +# This function will load external module form a given location and return a pointer to a function in a given module +# :path absolute path of the file (considered plugin) to be loaded +# :name name of the function to be applied +# """ +# _path = _args['path'] #os.sep.join([_args['root'],'plugin']) +# if os.path.isdir(_path): +# files = os.listdir(_path) +# if files : +# files = [name for name in files if name.endswith('.py')] +# if files: +# _path = os.sep.join([_path,files[0]]) +# else: +# return None +# else: +# return None +# #-- We have a file ... +# _name = _args['name'] - spec = importlib.util.spec_from_file_location(_name, _path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) +# 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 +# return getattr(module,_name) if hasattr(module,_name) else None -class Getter (Loader): - def __init__(self,**_args): - super().__init__(**_args) - def load(self): - super().load() - _system = self.system() - _logo = _system['logo'] - if 'source' in _system and 'id' in _system['source'] and (_system['source']['id'] == 'cloud'): +# class Getter (Loader): +# def __init__(self,**_args): +# super().__init__(**_args) +# def load(self): +# super().load() +# _system = self.system() +# _logo = _system['logo'] +# if 'source' in _system and 'id' in _system['source'] and (_system['source']['id'] == 'cloud'): - _icon = f'/api/cloud/download?doc=/{_logo}' - _system['icon'] = _icon +# _icon = f'/api/cloud/download?doc=/{_logo}' +# _system['icon'] = _icon - else: - _root = self._config['layout']['root'] - _icon = os.sep.join([_root,_logo]) - _system['icon'] = _logo +# else: +# _root = self._config['layout']['root'] +# _icon = os.sep.join([_root,_logo]) +# _system['icon'] = _logo - self._config['system'] = _system - if self._caller : - _system['caller'] = {'icon': self._caller.system()['icon']} - def html(self,uri,id,_args={},_system={}) : - """ - This function reads a given uri and returns the appropriate html document, and applies environment context +# self._config['system'] = _system +# if self._caller : +# _system['caller'] = {'icon': self._caller.system()['icon']} +# def html(self,uri,id,_args={},_system={}) : +# """ +# This function reads a given uri and returns the appropriate html document, and applies environment context - """ - _system = self._config['system'] - if 'source' in _system and _system['source']['id'] == 'cloud': - _html = cloud.html(uri,dict(_args,**{'system':_system})) +# """ +# _system = self._config['system'] +# if 'source' in _system and _system['source']['id'] == 'cloud': +# _html = cloud.html(uri,dict(_args,**{'system':_system})) - else: +# else: - _html = disk.html(uri,self.layout()) - # _html = (open(uri)).read() +# _html = disk.html(uri,self.layout()) +# # _html = (open(uri)).read() - #return ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>']) - _html = ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>']) - appContext = Environment(loader=BaseLoader()).from_string(_html) - _args['system'] = _system - # - # If the rendering of the HTML happens here we should plugin custom functions (at the very least) - # +# #return ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>']) +# _html = ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>']) +# appContext = Environment(loader=BaseLoader()).from_string(_html) +# _args['system'] = _system +# # +# # If the rendering of the HTML happens here we should plugin custom functions (at the very least) +# # - return appContext.render(**_args) - # return _html +# return appContext.render(**_args) +# # return _html - def data (self,_args): - """ - :store data-store parameters (data-transport, github.com/lnyemba/data-transport) - :query query to be applied against the store (expected data-frame) - """ - _store = _args['store'] - reader = transport.factory.instance(**_store) - _queries= copy.deepcopy(_store['query']) - _data = reader.read(**_queries) - return _data - def csv(self,uri) : - return pd.read(uri).to_html() +# def data (self,_args): +# """ +# :store data-store parameters (data-transport, github.com/lnyemba/data-transport) +# :query query to be applied against the store (expected data-frame) +# """ +# _store = _args['store'] +# reader = transport.factory.instance(**_store) +# _queries= copy.deepcopy(_store['query']) +# _data = reader.read(**_queries) +# return _data +# def csv(self,uri) : +# return pd.read(uri).to_html() - return _map - def menu(self): - return self._config['menu'] - def plugins(self): - return copy.deepcopy(self._plugins) if 'plugins' in self._config else {} - def context(self): - """ - adding custom variables functions to Jinja2, this function should be called after plugins are loaded - """ - _plugins = self.plugins() - # if not location: - # env = Environment(loader=BaseLoader()) - # else: - location = self._config['layout']['root'] - # env = Environment(loader=FileSystemLoader(location)) - env = Environment(loader=BaseLoader()) - # env.globals['routes'] = _config['plugins'] - return env - def config(self): - return copy.deepcopy(self._config) - def system(self,skip=[]): - """ - :skip keys to ignore in the object ... - """ - _data = copy.deepcopy(self._config['system']) - _system = {} - if skip and _system: - for key in _data.keys() : - if key not in skip : - _system[key] = _data[key] - else: - _system= _data - return _system - def layout(self): - return self._config['layout'] - def get_app(self): - return self._config['system']['app'] +# return _map +# def menu(self): +# return self._config['menu'] +# def plugins(self): +# return copy.deepcopy(self._plugins) if 'plugins' in self._config else {} +# def context(self): +# """ +# adding custom variables functions to Jinja2, this function should be called after plugins are loaded +# """ +# _plugins = self.plugins() +# # if not location: +# # env = Environment(loader=BaseLoader()) +# # else: +# location = self._config['layout']['root'] +# # env = Environment(loader=FileSystemLoader(location)) +# env = Environment(loader=BaseLoader()) +# # env.globals['routes'] = _config['plugins'] +# return env +# def config(self): +# return copy.deepcopy(self._config) +# def system(self,skip=[]): +# """ +# :skip keys to ignore in the object ... +# """ +# _data = copy.deepcopy(self._config['system']) +# _system = {} +# if skip and _system: +# for key in _data.keys() : +# if key not in skip : +# _system[key] = _data[key] +# else: +# _system= _data +# return _system +# def layout(self): +# return self._config['layout'] +# def get_app(self): +# return self._config['system']['app'] -class Router : - def __init__(self,**_args) : +# class Router : +# def __init__(self,**_args) : - # _app = Getter (path = path) - _app = Getter (**_args) +# # _app = Getter (path = path) +# _app = Getter (**_args) - self._id = 'main' - # _app.load() - self._apps = {} - _system = _app.system() - if 'routes' in _system : - _system = _system['routes'] - for _name in _system : - _path = _system[_name]['path'] - self._apps[_name] = Getter(path=_path,caller=_app,location=_path) - self._apps['main'] = _app +# self._id = 'main' +# # _app.load() +# self._apps = {} +# _system = _app.system() +# if 'routes' in _system : +# _system = _system['routes'] +# for _name in _system : +# _path = _system[_name]['path'] +# self._apps[_name] = Getter(path=_path,caller=_app,location=_path) +# self._apps['main'] = _app - def set(self,_id): - self._id = _id - def get(self): +# 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'] +# return self._apps['main'] if self._id not in self._apps else self._apps[self._id] +# def get_main(self): +# return self._apps['main'] diff --git a/cms/engine/basic.py b/cms/engine/basic.py index 3ec6fe9..8dd5b2f 100644 --- a/cms/engine/basic.py +++ b/cms/engine/basic.py @@ -20,7 +20,7 @@ class Initializer : """ def __init__(self,**_args): self._config = {'system':{},'layout':{},'plugins':{}} - self._shared = False if not 'shared' in _args else _args['shared'] + # 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 {} @@ -29,8 +29,13 @@ class Initializer : self._ISCLOUD = False self._caller = None if 'caller' not in _args else _args['caller'] self._args = _args - + # if 'context' in _args : + # self._config self.reload() #-- this is an initial load of various components + # + # @TODO: + # Each module should submit it's routers to the parent, and adjust the references given the callers + # def reload(self): @@ -39,7 +44,7 @@ class Initializer : self._isource() self._imenu() self._iplugins() - + self._iroutes () # self._ISCLOUD = 'source' in self._config['system'] and self._config['system']['source']['id'] == 'cloud' @@ -52,7 +57,15 @@ class Initializer : return cloud else: return disk - + def _iroutes (self): + """ + Initializing routes to be submitted to the CMS Handler + The routes must be able to : + 1. submit an object (dependency to the cms) + 2. submit with the object a route associated + The CMS handler will resolve dependencies/redundancies + """ + pass def _imenu(self,**_args) : pass def _iplugins(self,**_args) : @@ -252,8 +265,9 @@ class Initializer : _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 + self._config['system']['caller'] = {'icon': '/main'+self._caller.system()['icon'].replace(_callerContext,'')} + _context = '/'.join([_callerContext,_context]) if _callerContext != '' else _context + if os.path.exists(_newpath) and not self._ISCLOUD: @@ -278,14 +292,15 @@ class Initializer : 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): @@ -312,6 +327,15 @@ class Module (Initializer): elif type(_stream) == io.StringIO : self._config = json.loads( _stream.read()) self._ISCLOUD = 'source' in self._config['system'] and self._config['system']['source'] and self._config['system']['source']['id'] == 'cloud' + if self._caller : + self._config['system']['parentContext'] = self._caller.system()['context'] + else: + self._config['system']['parentContext'] = self._config['system']['context'] + if 'context' in _args : + self._config['system']['context'] = _args['context'] + + if '/' in self._config['system']['context'] : + self._config['system']['context'] = self._config['system']['context'].split('/')[-1] # # # self._name = self._config['system']['name'] if 'name' in self._config['system'] else _args['name'] @@ -361,6 +385,7 @@ class Module (Initializer): 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) @@ -402,7 +427,6 @@ class CMS: _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 diff --git a/cms/index.py b/cms/index.py index 5fbe446..b6be9e3 100644 --- a/cms/index.py +++ b/cms/index.py @@ -22,35 +22,34 @@ from typing import Optional import pandas as pd import uuid import datetime - +import requests 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') +@_app.route('/<id>/favicon.ico') +def favicon(id): + global _route + # _system = _route.get ().system() + # _handler = _route.get() + _handler = _getHandler(id) + _system = _handler.system() + _logo =_system['icon'] #if 'icon' in _system else 'static/img/logo.svg' + _stream = requests.get(''.join([request.host_url,_logo])) + + return "_stream",200,{"Content-Type":"image/png"} #_handler.get(_logo),200,{"content-type":"image/png"} + +def _getHandler (app_id,resource=None) : + global _route + _id = _getId(app_id,resource) + return _route._apps[_id] -def _setHandler (id) : - session['app_id'] = id +def _getId(app_id,resource): + return '/'.join([app_id,resource]) if resource else app_id +def _setHandler (app_id,resource) : + session['app_id'] = _getId(app_id,resource) + @_app.route("/robots.txt") def robots_txt(): """ @@ -74,27 +73,56 @@ def robots_txt(): ''') # return '\n'.join(_info),200,{'Content-Type':'plain/text'} - return Response('\n'.join(_info), mimetype='text/plain') + return Response('\n'.join(_info), mimetype='text/plain') +def _getIndex (app_id ,resource=None): + _handler = _getHandler(app_id,resource) + _layout = _handler.layout() + _status_code = 200 + global _route + _index_page = 'index.html' + _args = {} + + try : + _uri = os.sep.join([_layout['root'],_layout['index']]) + _id = _getId(app_id,resource) + _args = _route.render(_uri,'index',_id) + + except Exception as e: + _status_code = 404 + _index_page = '404.html' + print(e) + return render_template(_index_page,**_args),_status_code @_app.route("/") def _index (): - _handler = _getHandler() - _config = _handler.config() - global _route + return _getIndex('main') +# def _xindex (): +# _handler = _getHandler() +# _config = _handler.config() +# global _route - print ([' serving ',session.get('app_id','NA'),_handler.layout()['root']]) - _args={'system':_handler.system(skip=['source','app','data']),'layout':_handler.layout()} - try: - uri = os.sep.join([_config['layout']['root'], _config['layout']['index']]) - _index_page = "index.html" - print ([_route]) - _args = _route.render(uri,'index',session.get('app_id','main')) - except Exception as e: - # print () - print (e) - _index_page = "404.html" +# # print ([' serving ',session.get('app_id','NA'),_handler.layout()['root']]) +# _args={'system':_handler.system(skip=['source','app','data']),'layout':_handler.layout()} - return render_template(_index_page,**_args),200 if _index_page != "404.html" else 200 - +# try: +# uri = os.sep.join([_config['layout']['root'], _config['layout']['index']]) +# _index_page = "index.html" + +# _args = _route.render(uri,'index',session.get('app_id','main')) +# # _setHandler('main') +# except Exception as e: +# # print () +# print (e) +# _index_page = "404.html" + +# return render_template(_index_page,**_args),200 if _index_page != "404.html" else 200 +@_app.route("/<app>/<resource>") +@_app.route("/<app>",defaults={'resource':None}) +def _aindex (app,resource=None): + _handler = _getHandler(app,resource) + _setHandler(app,resource) + _html,_code = _getIndex(app,resource) + + return _html,_code # @_app.route('/id/<uid>') # def people(uid): # """ @@ -115,23 +143,28 @@ def _dialog (): _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] + +@_app.route("/api/<module>/<name>",defaults={'app':'main','key':None}) +@_app.route("/<app>/api/<module>/<name>",defaults={'key':None}) +@_app.route("/<app>/<key>/<module>/<name>",defaults={'key':None}) + +def _delegate_call(app,key,module,name): + _handler = _getHandler(app,key) return _delegate(_handler,module,name) -@_app.route('/api/<module>/<name>') -def _api(module,name) : +# @_app.route('/api/<module>/<name>') +@_app.route("/<app_id>/<key>/api/<module>/<name>", methods=['GET']) +@_app.route("/api/<module>/<name>",defaults={'app_id':'main','key':None}) +@_app.route("/<app_id>/api/<module>/<name>",defaults={'key':None}) + +def _api(app_id,key,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() + + _handler = _getHandler( app_id,key) return _delegate(_handler,module,name) def _delegate(_handler,module,name): @@ -143,7 +176,7 @@ def _delegate(_handler,module,name): _context = _handler.system()['context'] if _context : uri = f'{_context}/{uri}' - + _mimeType = 'application/octet-stream' if uri not in _plugins : _data = {} _code = 404 @@ -163,12 +196,21 @@ def _delegate(_handler,module,name): _data = json.dumps(_data) _code = 200 if _data else 500 return _data,_code,{'Content-Type':_mimeType} -@_app.route("/api/<module>/<name>" , methods=['POST']) -def _post (module,name): + +# @_app.route("/api/<module>/<name>" , methods=['POST'],defaults={'app_id':'main','key':None}) +# @_app.route('/<app_id>/api/<module>/<name>',methods=['POST'],defaults={'key':None}) +# @_app.route('/<app_id>/<key>/api/<module>/<name>',methods=['POST'],defaults={'app_id':'main','key':None}) + +@_app.route("/<app_id>/<key>/api/<module>/<name>", methods=['POST']) +@_app.route("/api/<module>/<name>",defaults={'app_id':'main','key':None},methods=['POST']) +@_app.route("/<app_id>/api/<module>/<name>",defaults={'key':None},methods=['POST']) + +def _post (app_id,key,module,name): # global _config # global _route # _handler = _route.get() - _handler = _getHandler() + # app_id = '/'.join([app_id,key]) if key else app_id + _handler = _getHandler(app_id,key) return _delegate(_handler,module,name) @_app.route('/version') @@ -212,8 +254,11 @@ def reload(): # return "",200 # pass # return "",403 -@_app.route('/page',methods=['POST']) -def cms_page(): + +@_app.route('/page',methods=['POST'],defaults={'app_id':'main','key':None}) +@_app.route('/<app_id>/page',methods=['POST'],defaults={'key':None}) +@_app.route('/<app_id>/<key>/page',methods=['POST']) +def _POST_CMSPage(app_id,key): """ return the content of a folder formatted for a menu """ @@ -221,7 +266,9 @@ def cms_page(): global _route # _handler = _route.get() # _config = _handler.config() - _handler = _getHandler() + _handler = _getHandler(app_id,key) + _setHandler(app_id,key) + _config = _handler.config() # _uri = os.sep.join([_config['layout']['root'],request.headers['uri']]) _uri = request.headers['uri'] @@ -244,11 +291,13 @@ def cms_page(): if 'read?uri=' in _uri or 'download?doc=' in _uri : _uri = _uri.split('=')[1] - _args = _route.render(_uri,_id,session.get('app_id','main')) + _args = _route.render(_uri,_id,_getId(app_id,key)) #session.get(app_id,'main')) return _args[_id],200 # return _html,200 -@_app.route('/page') -def _cms_page (): +@_app.route('/page',defaults={'app_id':'main','resource':None}) +@_app.route('/<app_id>/page',defaults={'resource':None},methods=['GET']) +@_app.route('/<app_id>/<resource>/page',methods=['GET']) +def _cms_page (app_id,resource): # global _config global _route # _handler = _route.get() @@ -257,33 +306,33 @@ def _cms_page (): # _uri = os.sep.join([_config['layout']['root'],_uri]) _title = request.args['title'] if 'title' in request.args else '' - _args = _route.render(_uri,_title,session.get('app_id','main')) + _args = _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() +# @_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 : +# _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() +# _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() @@ -310,6 +359,7 @@ def start ( # _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() diff --git a/cms/static/js/bootup.js b/cms/static/js/bootup.js index d9b3101..015380c 100644 --- a/cms/static/js/bootup.js +++ b/cms/static/js/bootup.js @@ -12,43 +12,26 @@ bootup.CMSObserver = function(_sysId,_domId,_fileURI){ var http = HttpClient.instance() http.setHeader('uri',_fileURI) - if (sessionStorage[_sysId] != null){ + if (sessionStorage[_sysId] != null ){ var uri = sessionStorage[_sysId]+'/page' }else{ var uri = '/page' } + + if (window.location.pathname != '/'){ + uri = ([window.location.pathname,'page']).join('/') + } + try{ // 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') - // _domElement.className = 'busy-and-loading' - // jx.dom.append(_domId, _domElement) if (x.status == 200){ - // jx.dom.set.value(_domId,x.responseText) - // var _domElement = jx.dom.get.instance('div') - // _domElement.innerHTML = x.responseText - setTimeout(function(){ - // _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) diff --git a/cms/templates/header.html b/cms/templates/header.html index 26428a6..ee0bde5 100644 --- a/cms/templates/header.html +++ b/cms/templates/header.html @@ -1,5 +1,4 @@ - <div class="icon"> <img src="{{system.icon}}"> </div> diff --git a/cms/templates/index.html b/cms/templates/index.html index dc9a2eb..825a231 100644 --- a/cms/templates/index.html +++ b/cms/templates/index.html @@ -31,10 +31,10 @@ Vanderbilt University Medical Center <!-- <link href="{{system.context}}/static/css/themes/{{system.theme}}" rel="stylesheet" type="text/css"> --> <!-- <link href="{{system.context}}/static/css/icons.css" rel="stylesheet" type="text/css"> --> - <link href="{{system.context}}/static/css/icons.css" rel="stylesheet" type="text/css"> - <link href="{{system.context}}/static/css/source-code.css" rel="stylesheet" type="text/css"> - <link href="{{system.context}}/static/css/search.css" rel="stylesheet" type="text/css"> - <link href="{{system.context}}/static/css/dialog.css" rel="stylesheet" type="text/css"> + <link href="{{system.parentContext}}/static/css/icons.css" rel="stylesheet" type="text/css"> + <link href="{{system.parentContext}}/static/css/source-code.css" rel="stylesheet" type="text/css"> + <link href="{{system.parentContext}}/static/css/search.css" rel="stylesheet" type="text/css"> + <link href="{{system.parentContext}}/static/css/dialog.css" rel="stylesheet" type="text/css"> <!-- applying themes as can --> <link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/layout.css" rel="stylesheet" type="text/css"> @@ -44,16 +44,16 @@ Vanderbilt University Medical Center <link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/footer.css" rel="stylesheet" type="text/css"> <link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/pane.css" rel="stylesheet" type="text/css"> - <script src="{{system.context}}/static/js/jx/dom.js"></script> - <script src="{{system.context}}/static/js/jx/utils.js"></script> - <script src="{{system.context}}/static/js/jx/rpc.js"></script> - <script src="{{system.context}}/static/js/jx/ext/modal.js"></script> - <script src="{{system.context}}/static/js/jx/ext/math.js"></script> - <script src="{{system.context}}/static/js/jquery/jquery.js"></script> - <script src="{{system.context}}/static/js/menu.js"></script> - <script src="{{system.context}}/static/js/search.js"></script> - <script src="{{system.context}}/static/js/bootup.js"></script> - <script src="{{system.context}}/static/js/fontawesome/js/all.js"></script> + <script src="{{system.parentContext}}/static/js/jx/dom.js"></script> + <script src="{{system.parentContext}}/static/js/jx/utils.js"></script> + <script src="{{system.parentContext}}/static/js/jx/rpc.js"></script> + <script src="{{system.parentContext}}/static/js/jx/ext/modal.js"></script> + <script src="{{system.parentContext}}/static/js/jx/ext/math.js"></script> + <script src="{{system.parentContext}}/static/js/jquery/jquery.js"></script> + <script src="{{system.parentContext}}/static/js/menu.js"></script> + <script src="{{system.parentContext}}/static/js/search.js"></script> + <script src="{{system.parentContext}}/static/js/bootup.js"></script> + <script src="{{system.parentContext}}/static/js/fontawesome/js/all.js"></script> </head> <script> sessionStorage.setItem('{{system.id}}','{{system.context|safe}}') @@ -68,9 +68,9 @@ Vanderbilt University Medical Center }) </script> <body> - + <div class="main {{system.theme}}"> - <div id="header" class="header" onclick="window.location.href='{{system.context}}/'" style="cursor:pointer"> + <div id="header" class="header" onclick="window.location.href='{{system.parentContext}}'" style="cursor:pointer"> {%include "header.html" %} </div> diff --git a/cms/templates/menu.html b/cms/templates/menu.html index 085eadb..72f8e58 100644 --- a/cms/templates/menu.html +++ b/cms/templates/menu.html @@ -1,7 +1,7 @@ {%if system.portal %} <div class="icon active"> - <div align="center" class="button" onclick="window.open('{{system.context}}/set/main','_self')" style="display:grid; grid-template-columns:auto auto; gap:4px; align-items:center "> + <div align="center" class="button" onclick="window.open('{{system.parentContext}}/','_self')" style="display:grid; grid-template-columns:auto auto; gap:4px; align-items:center "> <i class="fa-solid fa-chevron-left" style="color:darkgray; display:block"></i> <img src="{{system.caller.icon}}" style="height:100%"/> </div> From da1dcbe90ebaa2f9c256bb0ed75b22bd99e767b4 Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Tue, 27 Aug 2024 15:41:09 -0500 Subject: [PATCH 10/18] documentation ... --- readme.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 readme.md diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..9bd0345 --- /dev/null +++ b/readme.md @@ -0,0 +1,44 @@ +### What is QCMS + +QCMS is a lightweight, flask CMS designed easy to get working and allowing users to be able to change/edit content using **nextcloud** or **owncloud** as they see fit if need be. + +QCMS comes with standard web frameworks and python frameworks out of the box + +|HTML/JS| Python Frameworks| +|---|---| +|FontAwesome<br>JQuery<br>ApexCharts<br> | Flask<br>Numpy<br>Pandas<br>data-transport + +**Features**, + +**QCMS** Lowers the barrier to entry by : +1. automatically building menus from folder structure +2. add content in HTML5 or Markdown format + +### Quickly Getting Started + +**1. Installation** + + pip install dev.the-phi.com/git/qcms/cms + +**2. Create your project** + + qcms create --title "my demo project" --subtitle "I love qcms" --port 8090 ./qcms-demo + +The content is created in ./qcms-demo/www/html + +**3. Boot the project** + + qcms bootup ./qcms-demo/qcms-manifest.json + +### Limitations + +- Can not yet migrate an existing project into **QCMS** +- Templates are all CSS/SCSS based (no Javascript) + + + +Things to update with qcms runner : + - system.icon location, when creating a project + - applying themes + - upgrade all plugins to return data-type ? + From 0f6d5863903a5a43eedf4d9565b585e13da00443 Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Tue, 3 Sep 2024 15:35:37 -0500 Subject: [PATCH 11/18] bug fixes, added tabs (formalized) --- bin/qcms | 3 + cms/static/js/bootup.js | 6 ++ cms/static/js/menu.js | 224 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+) diff --git a/bin/qcms b/bin/qcms index 2cfc100..74c873b 100755 --- a/bin/qcms +++ b/bin/qcms @@ -293,6 +293,9 @@ def bootup ( """ This function will launch a site/project given the location of the manifest file """ + if not manifest.endswith('json') and os.path.isdir(manifest): + manifest = manifest if not manifest.endswith(os.sep) else os.sep.join(manifest.split(os.sep)[:-1]) + manifest = os.sep.join([manifest,'qcms-manifest.json']) index.start(manifest,port) @cli.command(name='theme') def handle_theme ( diff --git a/cms/static/js/bootup.js b/cms/static/js/bootup.js index 015380c..6fc6a3f 100644 --- a/cms/static/js/bootup.js +++ b/cms/static/js/bootup.js @@ -31,6 +31,12 @@ bootup.CMSObserver = function(_sysId,_domId,_fileURI){ if (x.status == 200){ setTimeout(function(){ + _content = $(x.responseText) + var _id = $(_content).attr('id') + _pid = (['#',_domId,' #',_id]).join('') + if( $(_pid).length != 0){ + $(_pid).remove() + } $('#'+_domId).append(x.responseText) },1500) diff --git a/cms/static/js/menu.js b/cms/static/js/menu.js index 55c1b46..4586689 100644 --- a/cms/static/js/menu.js +++ b/cms/static/js/menu.js @@ -106,4 +106,228 @@ menu.runScript = function(_id){ } }) } +menu.events = {} +menu.events._dialog = function (_item,_context){ + // var url = _args['url'] + _item.type = (_item.type == null)? 'redirect' :_item.type + var http = HttpClient.instance() + http.setHeader('uri',_item.uri) + http.setHeader('dom',(_item.title)?_item.title:'dialog') + // http.setHeader('dom',_args.text) + http.get(_context+'/dialog',function(x){ + + jx.modal.show({html:x.responseText,id:'dialog'}) + // menu.events.finalize ('.jxmodal') + }) + +} +menu.events._open = function (id,uri,_context){ + id = id.replace(/ /g,'-') + + var pid = '#content' + + + $('.content').children().slideUp() + $('#'+id).remove() + var httpclient = HttpClient.instance() + _context = (_context == null)?'':_context; + httpclient.setHeader('uri',uri) + httpclient.setHeader('dom',id) + httpclient.post(_context+'/page',function(x){ + var _html = x.responseText + var _dom = $(_html) + + + if(jx.dom.exists(pid) && jx.dom.exists(id)){ + jx.dom.remove(id) + + } + $(pid).append(_dom) + + // jx.dom.append(pid,_dom) + // $('#'+id).show('fast',function(){ + // $('#'+pid).slideUp() + // }) + var ref = pid + ' #'+id + $(pid).children().slideUp('fast', function(){ + $(ref ).slideDown('fast',function(){ + + $(pid).slideDown('fast',function(){ + var input = $(pid).find('input') + if (input.length > 0 ){ + $(input[0]).focus() + } + }) + + }) + + }) + + menu.events.finalize (ref) + + + // $('.content').append(_dom) + }) + +} + +menu.utils = {} +menu.utils.format = function(text){ + return text.replace(/(-|_)/g,' ').trim() +} +menu.events.finalize = function (ref) { + var scripts = $(ref+' script') + jx.utils.patterns.visitor(scripts,function(_item){ + if(_item.text.trim().length > 0){ + + var _code = eval(_item.text) + var id = ref + if (_item.parentNode != null){ + var id = _item.parentNode.id == null?_item.parentNode.className : _item.parentNode.id + } + id = (id != null)?id : ref + + // _delegate.scripts[id] = _code + } + }) +} + +/** + * Let's build the tab handling here ... + * + */ +var QCMSBasic= function(_layout,_context,_clickEvent) { + this._layout = _layout + this._context= _context + this._make = function (_items){ + var _panes = [] + var _context = this._context ; + _items.forEach(_item=>{ + var _div = jx.dom.get .instance('DIV') + + _div.innerHTML = menu.utils.format(_item.text) + + // + // We need to check for the override text and see if it goes here + _div.className = 'active' + _div.data = _item + _panes.push(_div) + $(_div).on('click', function (){ + // + // how do we process this ... + + if(this.data.uri) { + + if (this.data.type == 'dialog') { + menu.events._dialog(this.data,_context) + }else{ + menu.events._open(menu.utils.format(this.data.text),this.data.uri,_context) + } + }else{ + window.open(this.data.url,menu.utils.format(this.data.text)) + } + }) + + + }) + return _panes ; + + } + this.init = function (){ + var _make = this._make + var _layout = this._layout + Object.keys(this._layout.menu).forEach(function(_name){ + var _div = _make(_layout.menu[_name]) ; + + + var _sub = jx.dom.get.instance('DIV') + var _menuItem = jx.dom.get.instance('DIV') + _menuItem.innerHTML = menu.utils.format (_name) + _sub.className = 'sub-menu border-round border ' + _menuItem.className = 'item' + _div.forEach(_item=>{$(_sub).append(_item) }) + $(_sub).append(_div) + _menuItem.appendChild(_sub) + + $('.main .menu').append(_menuItem) + }) + } +} + +var QCMSTabs = function(_layout,_context,_clickEvent){ + // + // This object will make tabs in the area of the menu + // @TODO: we can parameterize where the menu is made to improve flexibility + // + this.tabs = jx.dom.get.instance('DIV') + this.tabs.className = 'tabs' + this._context = _context + this._make = function (text,_item,_event){ + var text = text.trim().replace(/(_|-)/ig,' ').trim() + var _context = this._context; + if (text.match(/\//)){ + text = text.split(/\//g).slice(-1)[0] + } + var _button = jx.dom.get.instance('INPUT') + var _label = jx.dom.get.instance('LABEL') + _button.type= 'radio' + _button.id = text+'tab' + _button.name = 'menu-tabs' + _label.innerHTML = text.toLowerCase() + _label._uri = _item[0].uri + _label.htmlFor = _button.id + $(_label).on('click',function (){ + + menu.events._open(this.innerHTML,this._uri,_context) + }) + // $(this.tabs).append( [_button,_label]) + return [_button,_label] + + } + + this._layout = _layout + this.init = function (){ + var _make = this._make + var tabs = this.tabs + Object.keys(_layout.menu).forEach(function(_key){ + _item = _layout.menu[_key] + // console.log([_item]) + _tabItem = _make(_key,_item) + $(tabs).append(_tabItem) + }) + + this.tabs.className = 'tabs' + $('.main .menu').append(this.tabs) + $('.main .menu').css({'border':'1px solid transparent'}) + } + // + // We need to load the pages here ... + // + + + +} + +menu.tabs = { } +// menu.tabs.make = function(text,_clickEvent){ +// var _id = text.trim() +// if (text.match(/\//)){ +// _id = text.split(/\//g).slice(-1)[0] +// } +// var _button = jx.dom.get.instance('div') +// var _label = jx.dom.get.instance('LABEL') +// _button.type= 'radio' +// _button.id = _id + +// _label.innerHTML = _id.toLowerCase() +// $(_label).on('click',_clickEvent) +// return [_button,_label] +// } +menu.tabs.init =function (_layout){ + // console.log(_layout) +// var _tabs = new QCMSTabs (_layout) + var _tabs = new QCMSBasic (_layout) + _tabs.init() + +} From 5590fec6c661fceea442951a6bac87e35045e825 Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Tue, 3 Sep 2024 15:41:02 -0500 Subject: [PATCH 12/18] menu/tabs handling automatic --- cms/static/js/menu.js | 19 +++++++++++++++++-- cms/templates/menu.html | 40 ++++++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/cms/static/js/menu.js b/cms/static/js/menu.js index 4586689..6304365 100644 --- a/cms/static/js/menu.js +++ b/cms/static/js/menu.js @@ -324,10 +324,25 @@ menu.tabs = { } // return [_button,_label] // } menu.tabs.init =function (_layout){ + // + // Let us determine what kind of menu is suited for this + // @TODO: Make menus configurable i.e on other areas of the site + // + var _count = 0 + var _items = 0 + Object.keys(_layout.menu).forEach(_name=>{ + _items += _layout.menu[_name].length + _count += 1 + }) + if (_count == _items){ + var _menuObject = new QCMSTabs (_layout) + }else{ + var _menuObject = new QCMSBasic (_layout) + } // console.log(_layout) // var _tabs = new QCMSTabs (_layout) - var _tabs = new QCMSBasic (_layout) - _tabs.init() + + _menuObject.init() } diff --git a/cms/templates/menu.html b/cms/templates/menu.html index 72f8e58..b47f6b3 100644 --- a/cms/templates/menu.html +++ b/cms/templates/menu.html @@ -11,33 +11,37 @@ <i class="fa-solid fa-home"></i> </div> {% endif %} -{% for _name in layout.menu %} +<!--{% for _name in layout.menu %} <div class="item"> <div> <i class="{{layout.icons[_name]}}"></i> {{_name.replace('-', ' ').replace('_',' ')}} </div> {%if layout.menu[_name] %} - <div class="sub-menu border-round border"> - - {% for _item in layout.menu[_name] %} - - {%if _item.type == 'open' %} - <div class="active" onclick="window.open('{{_item.uri}}','_self')"> - - {%elif _item.uri and _item.type not in ['dialog','embed'] %} - <div class="active" onclick="menu.apply('{{_item.uri}}','{{_item.text}}','{{_name}}','{{system.context}}')"> - {% else %} - <!-- working on links/widgets --> - <div class="active" onclick='menu.apply_link({{_item|tojson}},"{{system.context}}")'> - {% endif %} - <i class="fa-solid fa-chevron-right" style="margin-right:4px"></i> - {{_item.text.replace('-',' ').replace('_',' ')}} + <div class="sub-menu border-round border"> + {% for _item in layout.menu[_name] %} + + {%if _item.type == 'open' %} + <div class="active" onclick="window.open('{{_item.uri}}','_self')"> + + {%elif _item.uri and _item.type not in ['dialog','embed'] %} + <div class="active" onclick="menu.apply('{{_item.uri}}','{{_item.text}}','{{_name}}','{{system.context}}')"> + {% else %} + <!-- working on links/widgets --> + <div class="active" onclick='menu.apply_link({{_item|tojson}},"{{system.context}}")'> + {% endif %} + <i class="fa-solid fa-chevron-right" style="margin-right:4px"></i> + {{_item.text.replace('-',' ').replace('_',' ')}} + </div> - {%endfor%} + {%endfor%} </div> {%endif%} </div> -{%endfor%} \ No newline at end of file +{%endfor%} +--> +<script> + menu.tabs.init({{layout|tojson}}) +</script> \ No newline at end of file From d5a6dbb713c7422a6e5cc9562c98b013c87455c7 Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Tue, 3 Sep 2024 17:04:22 -0500 Subject: [PATCH 13/18] bug fixes ... menu --- cms/disk.py | 2 ++ cms/static/js/menu.js | 3 ++- cms/templates/menu.html | 30 ------------------------------ meta/__init__.py | 2 +- 4 files changed, 5 insertions(+), 32 deletions(-) diff --git a/cms/disk.py b/cms/disk.py index da2be29..e1197ac 100644 --- a/cms/disk.py +++ b/cms/disk.py @@ -48,6 +48,7 @@ def build (_config, keep=[]): #(_path,_content): _path = _realpath(_path,_config) # print (_path) _items = folders(_path,_config) + _subItems = [ content (os.sep.join([_path,_name]),_config)for _name in _items ] _r = {} @@ -60,6 +61,7 @@ def build (_config, keep=[]): #(_path,_content): _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) diff --git a/cms/static/js/menu.js b/cms/static/js/menu.js index 6304365..5a90b67 100644 --- a/cms/static/js/menu.js +++ b/cms/static/js/menu.js @@ -334,6 +334,7 @@ menu.tabs.init =function (_layout){ _items += _layout.menu[_name].length _count += 1 }) + if (_count == _items){ var _menuObject = new QCMSTabs (_layout) }else{ @@ -341,7 +342,7 @@ menu.tabs.init =function (_layout){ } // console.log(_layout) // var _tabs = new QCMSTabs (_layout) - + console.log(_menuObject) _menuObject.init() } diff --git a/cms/templates/menu.html b/cms/templates/menu.html index b47f6b3..74ef8e0 100644 --- a/cms/templates/menu.html +++ b/cms/templates/menu.html @@ -11,37 +11,7 @@ <i class="fa-solid fa-home"></i> </div> {% endif %} -<!--{% for _name in layout.menu %} - <div class="item"> - <div> - <i class="{{layout.icons[_name]}}"></i> - {{_name.replace('-', ' ').replace('_',' ')}} - </div> - {%if layout.menu[_name] %} - <div class="sub-menu border-round border"> - - {% for _item in layout.menu[_name] %} - - {%if _item.type == 'open' %} - <div class="active" onclick="window.open('{{_item.uri}}','_self')"> - - {%elif _item.uri and _item.type not in ['dialog','embed'] %} - <div class="active" onclick="menu.apply('{{_item.uri}}','{{_item.text}}','{{_name}}','{{system.context}}')"> - {% else %} - <!-- working on links/widgets --> - <div class="active" onclick='menu.apply_link({{_item|tojson}},"{{system.context}}")'> - {% endif %} - <i class="fa-solid fa-chevron-right" style="margin-right:4px"></i> - {{_item.text.replace('-',' ').replace('_',' ')}} - - </div> - {%endfor%} - </div> - {%endif%} - </div> -{%endfor%} ---> <script> menu.tabs.init({{layout|tojson}}) </script> \ No newline at end of file diff --git a/meta/__init__.py b/meta/__init__.py index 61074d7..5a70ba9 100644 --- a/meta/__init__.py +++ b/meta/__init__.py @@ -1,5 +1,5 @@ __author__ = "Steve L. Nyemba" -__version__= "2.1.4" +__version__= "2.1.6" __email__ = "steve@the-phi.com" __license__=""" Copyright 2010 - 2024, Steve L. Nyemba, Vanderbilt University Medical Center From a54ad94d15abd0e9bb5be93c601f5d7fd40f0d6e Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Tue, 3 Sep 2024 17:11:01 -0500 Subject: [PATCH 14/18] bug fix: menu & application context --- cms/static/js/menu.js | 6 +++--- cms/templates/menu.html | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cms/static/js/menu.js b/cms/static/js/menu.js index 5a90b67..09bca25 100644 --- a/cms/static/js/menu.js +++ b/cms/static/js/menu.js @@ -323,7 +323,7 @@ menu.tabs = { } // $(_label).on('click',_clickEvent) // return [_button,_label] // } -menu.tabs.init =function (_layout){ +menu.tabs.init =function (_layout,_context){ // // Let us determine what kind of menu is suited for this // @TODO: Make menus configurable i.e on other areas of the site @@ -336,9 +336,9 @@ menu.tabs.init =function (_layout){ }) if (_count == _items){ - var _menuObject = new QCMSTabs (_layout) + var _menuObject = new QCMSTabs (_layout,_context) }else{ - var _menuObject = new QCMSBasic (_layout) + var _menuObject = new QCMSBasic (_layout,_context) } // console.log(_layout) // var _tabs = new QCMSTabs (_layout) diff --git a/cms/templates/menu.html b/cms/templates/menu.html index 74ef8e0..88cdba9 100644 --- a/cms/templates/menu.html +++ b/cms/templates/menu.html @@ -13,5 +13,5 @@ {% endif %} <script> - menu.tabs.init({{layout|tojson}}) + menu.tabs.init({{layout|tojson}},{{system.context}}) </script> \ No newline at end of file From 2df876e9a8fc9ba9d7d99261c034c85ae630874f Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Tue, 3 Sep 2024 17:16:50 -0500 Subject: [PATCH 15/18] bug fix --- cms/templates/menu.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/templates/menu.html b/cms/templates/menu.html index 88cdba9..208f67b 100644 --- a/cms/templates/menu.html +++ b/cms/templates/menu.html @@ -13,5 +13,5 @@ {% endif %} <script> - menu.tabs.init({{layout|tojson}},{{system.context}}) + menu.tabs.init({{layout|tojson}},'{{system.context}}') </script> \ No newline at end of file From 54010cbbf3555d85763c6c7292a4fdbc757a079f Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Tue, 3 Sep 2024 17:30:25 -0500 Subject: [PATCH 16/18] bug fix: context in menu --- cms/static/js/menu.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cms/static/js/menu.js b/cms/static/js/menu.js index 09bca25..2f94a28 100644 --- a/cms/static/js/menu.js +++ b/cms/static/js/menu.js @@ -234,10 +234,11 @@ var QCMSBasic= function(_layout,_context,_clickEvent) { } this.init = function (){ + var _me = this ; var _make = this._make var _layout = this._layout Object.keys(this._layout.menu).forEach(function(_name){ - var _div = _make(_layout.menu[_name]) ; + var _div = me._make(_layout.menu[_name]) ; var _sub = jx.dom.get.instance('DIV') From d795687620ab62886c9297a4f3fee8a26af8c510 Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Tue, 3 Sep 2024 17:31:03 -0500 Subject: [PATCH 17/18] bug fix: context in menu --- cms/static/js/menu.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cms/static/js/menu.js b/cms/static/js/menu.js index 2f94a28..8f1ef59 100644 --- a/cms/static/js/menu.js +++ b/cms/static/js/menu.js @@ -238,7 +238,7 @@ var QCMSBasic= function(_layout,_context,_clickEvent) { var _make = this._make var _layout = this._layout Object.keys(this._layout.menu).forEach(function(_name){ - var _div = me._make(_layout.menu[_name]) ; + var _div = _me._make(_layout.menu[_name]) ; var _sub = jx.dom.get.instance('DIV') @@ -288,12 +288,13 @@ var QCMSTabs = function(_layout,_context,_clickEvent){ this._layout = _layout this.init = function (){ + var _me = this; var _make = this._make var tabs = this.tabs Object.keys(_layout.menu).forEach(function(_key){ _item = _layout.menu[_key] // console.log([_item]) - _tabItem = _make(_key,_item) + _tabItem = _me._make(_key,_item) $(tabs).append(_tabItem) }) From 87952d61fe49e022803abeda760b7d5420752a40 Mon Sep 17 00:00:00 2001 From: Steve Nyemba <nyemba@gmail.com> Date: Wed, 4 Sep 2024 10:52:32 -0500 Subject: [PATCH 18/18] bug fix: order of menu items/tabs layout --- bin/qcms | 2 +- cms/static/js/menu.js | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/bin/qcms b/bin/qcms index 74c873b..9d1da44 100755 --- a/bin/qcms +++ b/bin/qcms @@ -243,7 +243,7 @@ def create(folder:Annotated[str,typer.Argument(help="path of the project folder" root = 'www/html' _system = System() _layout = Layout() - _layout = _layout.build(root=root, footer = [{"text":term} for term in footer.split(',')],header={'title':title,'subtitle':subtitle} ) + _layout = _layout.build(root=root, order={'menu':[]}, footer = [{"text":term} for term in footer.split(',')],header={'title':title,'subtitle':subtitle} ) _system = System() _system = _system.build(version=version,port=port, context=context) diff --git a/cms/static/js/menu.js b/cms/static/js/menu.js index 8f1ef59..e0b32a8 100644 --- a/cms/static/js/menu.js +++ b/cms/static/js/menu.js @@ -237,7 +237,11 @@ var QCMSBasic= function(_layout,_context,_clickEvent) { var _me = this ; var _make = this._make var _layout = this._layout - Object.keys(this._layout.menu).forEach(function(_name){ + + var _names = _layout.order.menu.length > 0 ? _layout.order.menu : Object.keys(_layout.menu) + + // Object.keys(this._layout.menu) + _names.forEach(function(_name){ var _div = _me._make(_layout.menu[_name]) ; @@ -291,7 +295,9 @@ var QCMSTabs = function(_layout,_context,_clickEvent){ var _me = this; var _make = this._make var tabs = this.tabs - Object.keys(_layout.menu).forEach(function(_key){ + var _names = _layout.order.menu.length > 0 ? _layout.order.menu : Object.keys(_layout.menu) + // Object.keys(_layout.menu). + _names.forEach(function(_key){ _item = _layout.menu[_key] // console.log([_item]) _tabItem = _me._make(_key,_item) @@ -330,6 +336,17 @@ menu.tabs.init =function (_layout,_context){ // Let us determine what kind of menu is suited for this // @TODO: Make menus configurable i.e on other areas of the site // + + if (_layout.order != null){ + if (_layout.order.length == null && _layout.order.menu == null){ + _layout.order = {menu:[]} + + }else if (_layout.order.menu == null){ + _layout.order.menu = [] + } + }else{ + _layout.order = {menu:[]} + } var _count = 0 var _items = 0 Object.keys(_layout.menu).forEach(_name=>{