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,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAANpElEQVR4Xu2dB9AlRRHHwZxzjt+Zc0KhzIeKEXMqA/CZoBTFrBjQExVUTJhKVPQUscyomBU9M+acMB0mTKhlzvj/Vb0t11e7b2fmzU7uqq7b+96k7fnvhJ6e7l13aVS1BHat+u3by+/SAFA5CBoAGgAql0Dlr99GgAaAyiVQ+eu3EaABoHIJVP76tY0A11R/P1J8I/FFxD8Tv098pPhHNWKhJgA8VB38AvEZBjr6z/rbPuLjagNBTgDg6z1J/FeHTrqD8rxDvOp9/6nfry/+okP52WbJBQBnk4S/L/63eJt4++LZRPCnV6Ifii9lkHiH0uxpkK6YJLkA4ImS+DN7Uv+WnvnbOw164sZK83GDdF2Si+nhFIv0WSfNAQDnl4R/ID73gKQ/rb89XvzJFb3wAP32Kote2qq0H7NIn3XSHADwPEn4URNSPn4xInxjIB2Lu9dZ9NINlPYzFumzTpo6AC4t6X5HfBYDKbM+OEb8VPGPe+mvoudvGuQnyd/FFxD/yTB99slSB8B2SXg/Syn/TelfKj5cfOoiL4vALQblvEZp7m+QrpgkKQPg6pLyl8Ws4l3o98rE9PE9MUCaGkV+ojTX7oHGpc7s8qQMAOb1vQNJ9HOq5+7i/tQRqOq41aQKgJtILHOvxNH+HSt+l/i94tPidkWc2lMFANs7tHJz0tNU+LY5K8ih7BQBsLsE91zxucSXF6MFnIPYXdxL/JU5Cs+lzBQB0JfdmfSfvcRPEN9wBqH+R2W+UXyImJ1CdZQ6ALoOOZ0eHiE+Qsyzb/qHCjxK/Azxr3wXnnJ5uQCgk+EBenj5jAL9o8pm6whXoQzKDQD0/dvEd5kRBBTNKMBowKjA6FAs5QiAK6s3UO2GaDt6AU4hjxajai6OQghxDqF9QYXuNkfBI2WikWQh+oGAdQapKlcAsA5gPRCaTlCFtxNzaFQE5QoADnoOjtADb1ad94xQ72xV5goArHgPmk0qwwVjM3g1MXaJxVCuAOCcgPOCkMS08+CQFYaoKyYAqNvlAOYSyocN/5B591wy4+AItXRxtoIxAYCZ1v5iDDhsCGOPh9hk8JCWreCTPZSTXBGxAMBK+t3iV4htVvMm9v2+hfwbFXhZ8R98F5xCeTEAgC6fEzgsfiAWdI8R/2tCIBhsYLJ19sCC4yrZCwPXGay6GADYV2/32qU35DYO5/Pc01sGAkChEzbFodu7U3VeSVzMvn8ZWaEFemY1gHP4jRGI/0J//6r4p2Ksc1l4YdUbiwArlsbFUmgA8CU/PxNpfl3tvJYYm4FiKSQAzikpcr/vQplI87ZqJ1NS0RQSAByvPikTaXKX8KaZtHWtZoYCAM4Y+PpdVvCYaqEzYO2AQwdGEI6E7ytmgTYHYZB64hwFp1ZmKAC4KG/Yd6N6xWZvbB6+s357mRiA+SJ0/ucTYxHERREWon8Rszjl8khRFAIAl5PEuM59RgvJYYhxKzFf/RShGt4hRlnjixh1UDX3fQoAQuwBOIRiNCuCQgBgiySFjR1fqwmhB9gq/pRJ4kWaK+hflEtntcjjmvS3yngzMSNC9hQCAJ2QmFefJZ46xUPrxnbRllAkPcU2k2N6RiZc1mRvLxgSAJ2sOQfAoKNTBS/3wWX0BxePXXj2YOpwvUxqiwUMQzAQyZpiAACBcR7AKv5QMT4AOsITCGsGV/raCmC5ljmWz/Ygy3f9XsqLBYCu8aiGWemjH0D1+yHxLdd4M1eTcerFFPw+FnW/XWnvapE+yaSxAdAJhXuAjxVfVbyOzf9bHTqFrR2LSObzk8XsKkyI7eeBJglTTpMKADoZsYp38QPY5f+SHti72xBOpF69yPBK/ftAw8zsavA9mDWlBoB1hMkUgqaQC6WmhH7iGuLu0gfrDxxNMTWtIoC2h3jKhsG0HdHSlQSAx0mKz7aU5NBXfG+Vgep5bDfBUfWe4iKUQaUA4JLqEI5vh3wJjmECJxRjV85xJs2x9fV6mVkjsMbAZd0vLYGWbPISAIDe/qOLodxG0CikPjGRAfUyB07cGmZqQAtYFOUOAL5UzMtQHtkQBqm3t8lQatqcAcAcjb9gto8Ym5gShzpY+jBlVE85A6DrPHwJA4KHi6d8AZKHEWOz+p5fCKAEAHR9yUKQyxvs68dW8CzkMCap0h/QEOhLAkD3flgRbxNzj2CZWNk/un39/5NAiQDo3g6v3xw/Ey8AYiWPoqcqJ1BTYC8ZAN2730IPfPlvEveDTkzJporfawAAHYl5F+uCYm/4uKK1FgC4yqf4fA0AxXfx6hdsAGgAqFwClb9+aiPAOdQfrNo3xLhl+awYO79UCIOVmy+2k9gC4K+QYBPZXiBNBQC0A2fQBHxaPtIlJBxWOt+NjIJN1f8c8QWX2gFAH7QAQuQm2lefCgBerKYT23eMfqcfUOiYRv+yl8TqHHgJPWxFEvwccZPJJkCl7zY6lZcCADiWJWzLFPGlXUcc2mcvRiFMRVOywhwN41LuEWZDUy8V4kV2qBLTq9i3Udr3h2hUr4436JnIIibUNzA1SR89TWwAoKHDCtjU5x83irABCEl82dw6MqHsjppjA4AFH/H9TCmGt06uiZv6NSCY9Z1MXyaFdLEBgAzwA2Bq0cMNolWLsTlkykXQKxoW/BKle5hh2iSSpQAAmzmWG7mh9QJEMDO1Ibi10mYVUyAFAGB1S0CGKXOutyjNPSJ8NhdVnWw/zztRNxHHMVLNSimUAgCQK6tsFlBjXkTQuLHPjmWWzYXV48RjMQyJT4yGMDsXMqkAABCwuh8y2MCSZyNi53cf/v300N0hXB4MaN/JEUantav0CQC+DldnTRhr4N0DnwHLhBEHyiJ8B7gQhqBc5+LOoOnN3+V6kBOaStTVQ4SzCEYpF2LK2OmS0UcenwBASUMQ5tSIRSOLRzjFMLGoubndFIUaAKKI/f8qbQCYuQ/aCLBCwG0EmBl9BsW3EcBASOskaSNAGwHaInAMA20KWGds8ZO3TQF+5DhaSpsC2hTQpoA2BTRF0CAG2hpg5vnHoPii1gAYd2IVgxtV3KljIIn/vuuKOfHrrmobyMVbkv4aABt+PH3RRlzEcPeA9uEy5m5i1NmhqRgA4HkD275VvncQMMEfLxxQyh0ACDWDCRpHt2OETwF8BPoMPjH1qsUAYOpFu9+JwoH9fN9LuGlel3QdALq8F9cDsYf3EmPswUnh8WLMuTBPw+fQR8R4EA1BywDYoko5ecS+gNGJWIpYQr9I7N25hc81gI2wGHIZjm3CyNiU30/bBwDHynzh5xko7Of62x3FHOtuiJnCcGI9N2EU21kbcRx+lHjI8ASgYBH1YZ8NigUA3sElkJTLu5+qTAzpHAdjr7fK9IxA0Vw+wbIHh1NPd6nQIQ/rI46E+cpXBbxgzULkFW8u7mICIOT5PHcPMAgxiSbCCLGfmHUKowLBLVKiD6oxmMd5oZgA4AXwuZtaJFG+MtYBWCJhQAJQUyIsiFi7eFkPxAYA8+1uKUl30RbuAZwkZluboktZEz/HRmKNDYAUvzAEx5oBZ5KYuMXQDUx1HttVzNDXppgAYG5l0TVlb7/2S1oWwGqbaQkHEASUQL+REhHZlPbZXKkbbX9MAIBim+CQ63TCTmVmlW2yrTtS6bD+3RC7hK9bp50meQlVhxWyF4oJAGICe3uRFdJAkYLyiRjAeBtZNeLQ4aituYCCc0mXAJa2HcOijo+BdqGmXnUR9deL9hEf0QvFAgBaOPbkIervK4J2V514DN0YkB7X0wgDBwgIPMUcOxU7yEcn9BVBW1XgseKh6+iEqCGimjcdAI0P0QHLQkLRQpy+ULbwy6pgvjAUL50qmK+JABLcPcT7CCMF2rZ+4GgfHT1WRh8ApOG8gviFqIKZ608R87G8Xuzd06lPALCdY3+KMIcIte/+Yhwtjd2x+7Z+45yAl8ZjGKtxOopnV+oAwK0gOpfwMkPEopSAUawBxsDJ6MA5AecHjA5EKqF96yxklwHg+p5O+XwCoLsZxFDKRUq2eNzrA8X42eG4dWOklTv0d6J+fX7gd1yzcXhziNgmKFRX1LJJ2In6gXh/DKW0D69fjEoM/9xUHiLCxBGUgs5fJoC9KUZt7HLKWRwARmQ4+ucj9MvB4qlr1XQOo4vtUe26NoHcWj5APDX8csrIqaJt4MqqAXC0BGYaqRME4YWLL9hmyF0HAO9RXZwQmnomYwRgFCN6iSlVCwDmeeZkdO82xFk5fgVNyRUAtIv20U4bIhglFlGmVC0AGPZtI30iVOZcOoUDGxNyBQAgO8ikgoE0NhrEagGAihUHTC7Eke0+hhldAcDq3tX4And2ANyEqgQAcyrbKNO5dVmQY95EhgTuCgC2jfgIdCHsCbYbZqwSAGy/TPTyYzI8UD9gw2dCrgBgy4mNoAvhK5CtsAlVCYDTJBmUO65+dQ9VXvQCJuQKABaArhHC2Tbi1NKEqgQAgiEuwAkmEhpIg1kUc7QJuQJgXxV+jEkFA2kwfd80zFstABAuQrYl9tgYa5j6F3YFAOAEpLaE11NUxaZTXLUAwOBiDzFqVhtCM2cDHFcA0Ka9xSiDbMhmB0C5xQDARkgtbSIS8HkYlMgrtWbYSKABwEZaBaZtACiwU21eqQHARloFpm0AKLBTbV6pAcBGWgWmbQAosFNtXqkBwEZaBab9L2FjSp+s5clTAAAAAElFTkSuQmCC"""
+ _image1 = """data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAACXBIWXMAAAsSAAALEgHS3X78AAAABmJLR0QA/wD/AP+gvaeTAAAcdklEQVR4nOzdCawtSV0G8GIGGGSTRWSCwQ2IaNwQEUUBRcUwxgUxEYmChiAGJYNGXDDGBRc0qEF04oIBFVFExIVEhCAQA24oohLBGDHiMgKyiILICFbPmSczj3f7LK+rv+6u3y/5okbxVlfX7e//7j33nFIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYlXd+aXnfKUmvGwA40qmlbxAAgBWasvgNAgCwcC2L3yAAAAs0Z/kbBABgAVLlbwgAgJB0+RsCAGBm6dI3BADAzNJlbwgAgJmlS94AAEBcb+WTLnhDAACL0FsBpcvdEABAXG8llC51AwAAi9BbGaVL3RAAQFxvZZQucwMAAIvQWymly7ynvQZgoXosp3SZ97LPACxYbwWVLvGt7y8AK9BjUaVLfKv7CsCK9FhY6RLf2n4CsDK9Fle6xLeyjwCsUM/llS7xrewjACvUc4GlC3zt+wfASqULLF1k6ete674BsHLpAksXWvp617ZfAGxAuryWUGzp61zLPgGwIenyWkLBpa9vyXsDwAaly2spZZe+riXuCQAbli6vpRRe+pqWth8AbFi6uJZWfOnrWco+ALBx6eJaWgGmryN9/QB0IF1aSyzC9PqVPwDNpYtriaWYXrcBAICm0qW15HJMr9kAAEAz6dJackmm16r8AWgiXVpLL8z0Gg0AAEwuXVhLyFb2aI7zAsBGpEtrSVnz/sx9bgBYsXRpLTVr25v0OQJgZdLFJQYAAGaWLi1R/gAEpItLlD8AM0sXlyh+AALSBSbKH4CZpQtMFD8AAekiE8UPwMzSZSZKH4CAdLGJwgdgZumSE+UPwMzSJSfKH4CAdNGJAQCAmaVLTgwAAASkS04MAQDMLF1uYggAICBdbGIQACDgrIJJF50YBABYkHTxiUEAgLB0+YkhAICwdAmKQQCAoHQJikEAgJB0AYpBAICgdAGKQQCAkHT5iUEAgJB08YlBAICQdOmJQQCAkHThiSEAgJB04YlBAICQdNmJQQCAkHTRiUEAgIB0wYlBAICQdLmJQQCAkHSxiUEAgIB0oYkhAICQdKGJQQCAgHSRiUEAgIB0gYlBAICQdHmJQQCAgHRpiUEAgIB0WYlBAICQdFGJQQCAgHRBiSEAgIB0OYlBAICAdCmJIQCAgHQhiQEAgIB0IYnyByAkXUxiAAAgIF1MovwBmFm6mMQAAEBAuphE+QMQkC4nMQAAEJAuJ1H+AASkC0qUPwAB6ZISAwAAAemSEuUPwMzSJSUGAAAC0iUlyh+AgHRRiQEAgJmlS0qUPwAB6aISAwDA4mz9QZouKVH+AIvTw8M0XVRiAABYnK0/UNMlJcofYHF6eLCmi0oMAACL0sODNV1SovwBFmfrD9h0SYnyB1icHh606aISAwDA4mz9YZsuKVH+AIuz9QduuqTEAACwSFt+8KYLStolfbYAVm3LD+B0QUnbpM8XwKpt9SGcLidpm/T5Ali1LT6M08Uk8yR9zgBWbWsP5XQpyTxJnzOAVdvSwzldSDJvEmcMYDO28KBOF5HMn9ZnCmDT1vzQTheQZDPlWQLoSvoBfspDPL1eWUZafD8AdCP9EBc5JenvG4BVSz/ERU5N+nsHYNXSD3GRU5L+vgFYtfRDXOTUpL93AFYt/RAXOSXp7xuAVUs/xEVOTfp7B2DV0g9xkVOS/r4BWLX0Q1zk1KS/dwBWLf0QFzkl6e8bgFVLP8RFTk36ewdg1dIPcZFTkv6+AVi19ENc5NSkv3cAVi39EBc5JenvG4BVSz/ERU5J+vsGYNXSD3GRU5P+3gFYtfRDXOSUpL9vAFYt/RBfWuzTejLn9wnA5qQf4msukPT6e84UZx+gW+mH+JaKI31dvaXVfQToQvohvsXCSF9nD5nrXgJsUvohvvWySF/3lpO6pwCbkH6I91IS6X3Yaqbc+9ZnAGAx0g/vJZTDnNL7scW03Os5zwbArNIP70QxLEF6b7aYlvuaPi8Ak0o/sHt/WKf3SbZ7tgBGpR+mHtDbvAc9JH1uAE6WfoB6KL9fet+kr/MGdC798PRAvqH03kl/Zw7oVPrB6UF8Q+n9k37PHtCp9MPTA/j90vsoziDQofQD1IN3J72f4iwCnUo/RD1013cPZLtnEehQ+mHa8wM3va/iTAIsuozSe9NSem/FuQS4Vvqh2ttDNr2/4mwC/L/0g7W3h2x6j8X5BLgBD9d5pPdZnFGAC/JwbStdYuKMAozycG0nXWTinALs5aE6vXSJibMKcDAP1emkS0ycVYCjeahevHSJibMKcBIP1YuXLjJxVgFO5qF6unSRibMKcNE8UI+XLjJxXgEm44F6uHSRzV2a6TUYAABm4IG6X7rIUmWZXpMBAGAGHqhnSxdZqijT6zIAAMzIA/UDpYssWZTptRkAAGbkgXpD6SJLlmR6fQYAgAAP1J10kRkADAAAEb0/VNNFlizI9BqVPwAR6SJbQkmm12kAAGB26SJbQkGm12oAAGB26SJbQjmm16v8AZhdusyWUpDpNRsAAJhNusiWVI7pdRsAAJhNusiWVo7ptSt/AGaRLrMllmP6GpQ/AE2ly2ypBZm+BgMAAE2ly2zJ5Zi+FuUPQBPpMltDQaavR/kDMLl0oa2lINPXpfwBmEy60NZWklu9LgA6ki7qtZbkFq8JgI6ky3rtZbm16wGgA+mi3lJZbulaANiwdFFvtTC3cA0AbFS6qLc8AJxvjWsGYIPSJd1T+QPAIqRL2gAAADNLF7TyB4AZpcvZAAAAM0sXs/IHgBmlS9kAAAAzSxey8geAkHQxGwAAIChd0MofAILSZW0AAICQdGErfwAISpe3AQAAgtIlrvwBIChd6MofAILS5W4AAICgdMkrfwAIShe+AQAAQtKlr/wBIMgAAAAdU/4A0DEDAAB0TPkDQMcMAADQMeUPAJ0yAABAx5Q/AHRM+QNAxwwAANAx5Q8AHTMAAEDHlD8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADADG5U80k1j6n5yZoX1Lyy5nU1f3fdf//7NT9T880196m5cWSlbNIDv+/57zs26TUDrNVQ+p9ZdqX+ppr3HZl31PxqzRXFMMAJTil9wwDA6S6p+fKaPy/Hl/5Z+fuaR9dcNuN1sFJTFr9BAOAw96h5RZmu+M/P8OuCK2a7GlalZfEbBAAu7NKaJ9ZcU9qV//Xz9JpbzHJlrMKc5W8IANi5Xc1LyzzFf/28puaj218eS5cof0MA0Ls71by6zF/+53J12f3agQ4li98gAPTsDmX3Z3yp8j+Xt5XdnxjSmXTpGwCAHt2y5lUlX/7n8oaaD2t6xSxKuvANAUCvfrHkS//8/GHNTVpeNMuQLnpDANCrR5R82Z+V72l32SxBuuANAUCvhlf8v7Hki/6svLvm7s2unrh0uRsAgF79VMmX/L48v9nVE5UudkMA0KvLa95V8gV/SO7VaA8ISpe6AQDo1Q+XfLEfmmc32gNC0oVuCAB6NbzV77+UfLEfmuG1ALdvshNEpMvcAAD06oElX+rH5lFNdoKIdJkbAIBePbnkC/3YPKfJTjC7dJEbAoCe/VnJF/qxeXPNjVpsBvNKl7gBAOjVjWv+u+QL/ZTcucF+MLN0iRsAgF7dreSL/NR8XoP9YGbpEjcAAL16QMkX+an52gb7wczSJW4AAHr1JSVf5Kfmygb7wczSJW4AAHr10JIv8lPzHQ32g5mlS9wAAPTqi0q+yE/NYxvsBzNLl7gBAOjV/Uq+yE/NVzfYD2aWLnEDANCrjyn5Ij81D2ywH8wsXeIGAKBXw+cA/FfJl/kpubzBfjCzdIkbAICe/UnJl/mxeVOTnWB26RI3AAA9+5GSL/Rj47MANiJd4gYAoGf3LflCPzYPb7ITRKSLXPkDvRpeB3B1yZf6oXl3zYc02Qki0mVuAAB69r0lX+yH5pmN9oCQdJkbAICefWjNu0q+3A/JvRvtAUHpQlf+QM+eUvLlvi8vaHb1RKVL3QAA9Oy2ZffndemSPyvvqfn4ZldPXLrYlT/Qs+EjdtNFf1Z+uOF1swDpcjcAAL37pZIv+/MzvFnRTVteNMuQLnjlD/TsVjWvKfnSP5fh1xIf1fSKWZR00St/oGcfVvP3JV/+b6/51MbXygKlC1/5Az27S80/lGz536/1RbJM6dI3AAC9G95x72Vl/vJ/Q809Zrg+Fk7xA+TcrOa5Zb7y/7Oye2MiuJbyB8i5Sc2rS/vyf2vN7We6JlZE+QPkfH1pPwA8dbarYVUMAAA5X1DaDwBXznY1rI7yB8h4TGk/APzobFfDqvgJAEDGJWWe1wC8sezeiAhuwAAAML/hrwCeXtqX/7m8uOw+mAiuNWf5GwKACxlK6ds6y1Vl9zf5c5X/ubyl5udrnrCAPZgzo295nChC6Stj5w96drcyfxFKXxleaHmmdDnI9jN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnBgBpHQOARDN2/qBnpwwA/1Pz5zW/XvNjNd9W8401X3fdfx3+5yfV/HLNn9b8xwlfQ7YTA4BEM3b+oGeHDADvqXlpzbfWfFrNZSd8nY+teUzZDQ0Ggr4y+QAw9v9vqq8h28mx5wV6MTYA/GXN42ruMPHXvHnNV9W8sOa9I19ftpGLHgDG/vOnSBeSzJupzw9sxYUGgJeWPQ/tCX1CzbNqrrnAOmQbOWkAGPvPTCVdTGIAgKTrDwCvrXlQaB13r3lRyZWULGQAGPu/bSVdUGIAgIRhABhe1PddNTcJr2Xw0JqrS760ZOYBYOz/Zi7pohIDAMxp+P3+PdOLOM/lNS8u+eKSmQaAsf/93NJlJQYA6N2lNd9f8uUljQeApUqXlhgAoHePLl4guPa0GACGX1fdo+ZRNU+suarm2TW/UPOUmseX3etZ7nQxXyRdXLLMASD99aEnDym79yJIF5lkB4Db1zyy5ndr/vuIr/+amifXfNIpXzRdXrKMAWBJa4HePKJ4z4C15mIHgLvW/Gw5rvTPyvDOlF9ec8kxC0gXmORKd4lrgh4N70SYLjOZbwC4XdkVf4tfAQ1vcnW/YxaTLjGZt2yXvDbo1fB73nShSfsB4Ctq/q3xuoafKA0Dxs0PXVS6yKR9yS59fdCzW9f8XcmXmhyeYwaAG5fdB0zNub6/qvmYQxeYLjNpV7BrWCP07tNr/rfki00Oy6EDwPAv8d8LrfHNZfdBV3uly0zalGt6jcesFXo3/Og2XWxyWA4ZAIZPmEyV/7m8o+YzDljrYspCpinV9PqOWSuw+5Ow4V9t6XKT/fnCM+7hOTeqed4C1jlkOFN337Pea6WLQqYp1vS6jlkr8H5PKPnCkP258qwbeJ3HL2CN18/ram65Z82LLA45rlTTazp2vcD7fXDNW0u+MGQ8wxvxXHrGPfyssvswqvQaz88vnrHeG0iXhJxequm1HLte4AP9YMmXhezPs8oH/rndUP5vWcDazsre1y6kC0K2nX3nD3o3fJyxdwhcR4aPef6ZmieV3Qv+ln7f/rYc8BHZ6ZKQbWff+YPe/WHJl4VsM19b9kgXhGw7+84f9O6xJV8Uss0MLwgc/dyAdEHI9jN2/qB3H1fyRSHbzWeXPdIFIdvOvvMHPRv+jvxfSr4oZJt5WtkjXRCy/ew7g9AzHxIkrfKvZTdknildDrL9jJ0/6N33lHxRyHYz/JrpTOlykO1n7PxB776y5EtCtpuvKXukC0K2nX3nD3p2r5IvCdlufqjskS4I2Xb2nT/o2V1KviRkuxleYzIqXRCy/ew7g9CrDy35kpDt5gVlj3Q5yPaz7wxCrz6o5EtCtpuXlz3S5SDbz74zCL0aPhkwXRKy3byk7JEuB9l+9p1B6NWdS74kZLv5rbJHuhxk+9l3BqFX3g5YWubnyh7pcpDtZ98ZhF59fsmXhGw331L2SJeDbDv7zh/07BtLviRku3lQGZEuB9l+xs4f9O6pJV8Sss28p+bWZUS6HGT7GTt/0Lvhz7TSRSHbzCvKHulykO1n3xmEXt2y5n9Kvihkm7my7JEuB9l29p0/6NkVJV8Sss0Mg+Udyx7pgpBtZ9/5g579dMkXhWwzzyh7pMtBtp99ZxB6dVnNW0q+KGR7uabm7mWPdDnItrPv/EHPHlLyRSH78x8131rzkTW3qrlvzQsXsK6x/GTZI10Osv3sO4PQs5eVfFHIeIbfo9/nAvfukppfWcD6LpSra257gTXfQLocZNvZd/6gZ8O/ItNFIfvzs2fdwLL7FMe/WMAar5//rfmCkTVfK10OcvGFml7PMWsFbuhFJV8Wsj8PO+sGXuduNW9fwDrP5Yl71nutdEHINIWaXtex6wVK+ZKSLwo5LHv/NV12n+XwrgWs9ZdqbrRvselykGnLNL2+Y9cLPbt5zetLvizksBwyAAyG93N4d3Cdz6656SELTReETFuo6fUds1bo3Y+XfKnJ4Tl0ABh8bs2bA2scPkvikkMWmC4IaVeqa1kn9Gr40f97S77U5PAcMwAMPqrmVTOt7T9rHn7owtIFIe2LdQ1rhB4NxeBNf9aXYweAwfCj+O+seWfDdQ3vQ3DXQxeULjGZr1yXvDbo0R1qXlfyZSbH55QB4Jy71PxCmfbDnl5d82XHLiRdYjJ/yS51XdCT4dP+/rjki0zmHwDO+fCaH6n5hxPXMLy48Lk1X1gOeJX/+dIFJtmiXdp6oBe3r/mjki8xyQ4A5wzlfa+y+/XAUOivL7s37jn/a7615uU1P1V2/9q/zalfMF1espzCXcIaoBcfUfM3JV9gspwB4CzDoDj8uuDOZfcTo4uWLi1Z3gBwzDlp+TVhKpemF3CGoTTeWPLlJesYACaVLixZzwAAaza8Detv19wxvZDrDK/+/oFy4R/rpjN8ot2/1vxT2f2IOb2etWR0AFjaQzpdVmIAgLkMA8DwDfKmmkeWA98MpZEHlGX8yP+vy+6NYR5Vc/+ay0fWfLuae9c8ouxepDa8WPGaBVzDknLQAJB+UKdL6pQSS69pLZn7LMFanBsAzuWVNQ+ceQ2fWPOckimnIcNPG15c8zVlmp+EfHDZvfDsN0r2rW2XkoMHgMQDO11OU19/eu1LzFRnBbbm/AHgXP605qE1N2v0dYdXc9+/5jdL7p393lZ2v264c6NrHAw/Ifjmsvu1QbqIVzMAzPHwTpfSHNeZvqalpMXewhacNQCcy/DOe1eV3U8FLnYYGH69cM+y+7jV1+/5uq2L/wll9y/1uVxW8+jS5yBwUQPAlA/ydBEliyl9rb3sM6zJvgHg+hneN/1FNT9Y85CaTym7P7+6kOFPsT625sE13152Pw7/9yO+VosMP2l4Zhn/nX5rt6r50Zr3lOxerHIAOPThni6cJRdS+tp7229YsmMGgLFiHX5S8May3Pfqv7rM/9qGMZ9clvGCx9UPAGvLUaekkfQe9LjnsERTDABLz0tq7jTVhk3og2p+ruT3xwDQaQml96TnvYcl2PoA8PSaG0+2W21cWZb5vgcGgA4KKL03ve8/JG15AHjShPvU2leXaT/xbknpegA4+iQEpPfIPYCMrQ4A3z3lJs3kYWWbPwnodgA4/gjkpPfKfYD5bXEAuGrSHZrXY0p+/wwAnZZOes/cC5jX1gaA3ynZtzOewk+U/D4aADotnPTeuR8wny0NAG+o+ZBptyfiJjWvKPn9NAB0WjbpPXRPYB5bGQCGD+C5z8R7k/TRNW8v+X01AHRaNOm9dF+gva0MAD8x9cYswONKfl8NAJ0WTXov3RdobwsDwPAuf7eZemMW4NKaV5X8/hoAOi2Z9J66N9DWFgaAR06+K8vxOSW/vwaATksmvafuDbS19gHgH2tuOvmuLMsflPw+GwA6LZj03ro/0M7aB4BvmH5LFueKkt9nA0CnBZPeW/cH2lnzADB8PPEtp9+SRXpdye+3AaDTcknvsXsEbax5AHh6g/1Yqu8u+f02AHRaLuk9do+gjTUPAA9osB9LNbwvwHtLfs8NAJ0WS3qv3SeY3loHgLeV3Z/J9eSvS37fDQCdFkt6r90nmN5aB4DfbrEZC7fWzwgwAGxAeq/dJ5jeWgeAb2qxGQv34JLfdwNAx6WS3nP3Cqa11gHg/i02Y+E+ouT33QDQcamk99y9gmmtdQC4vMVmLNzwMcfvLPm9NwB0WirpPXevYFprHADe3mQn1uEvS37/DQCdlkp6z90rmNYaB4C/abIT6/C7Jb//BoBOSyW95+4VTGuNA8CfNNmJdfi1kt9/A0CnpZLec/cKprXGAeDFTXZiHZ5W8vtvAOi0VNJ77l7BtNY4ALygyU6sw1Ulv/8GgE5LJb3n7hVMa40DwMua7MQ6PKPk998A0GmppPfcvYJprXEAeGWTnViH55T8/hsAOi2V9J67VzCtNQ4A/9xkJ9bh5SW//waATkslvefuFUxrjQPA8Kl4N2uxGSvwTyW//waATkslvefuFUxrjQPAkI9rsRkLd4uaa0p+7w0AnZZKes/dK5jWWgeAh7fYjIW7b8nvuwGg41JJ77l7BdNa6wDw1BabsXCPK/l9NwB0WizpvXafYHprHQB6fDvg55f8vhsAOi2W9F67TzC9tQ4AQ+7aYD+W6rKad5T8nhsAOi2W9F67TzC9NQ8Aj2+wH0v1xSW/3waATsslvcfuEbSx5gHgtQ32Y6meV/L7bQDotFzSe+weQRtrHgCG3Gf6LVmcy2veXfJ7bQDotFzSe+weQRtrHwB+c/otWZwfKvl9NgB0WjDpvXV/oJ21DwDDuwJ+wuS7shy3qXlbye+zAaDTgknvrfsD7ax9ABjyosl3ZTmeXPL7awDotGTSe+reQFtbGACGXDH1xizAcG/W/Lt/A8DKpffUvYG2tjIAvL7m1hPvTdIlNS8p+X01AHRaNOm9dF+gva0MAEOeNvHeJF1Z8vtpAOi0bNJ76J4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH+D8AAAD//6wKjWMAAAAGSURBVAMAKTd7j/uwRkMAAAAASUVORK5CYII="""
+ _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
-"""
+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"," 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(['
'.replace(':id',id),_html,'
'])
-# _html = ' '.join(['
'.replace(':id',id),_html,'
'])
-# 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(' ','')
- 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//')
-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//" , 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 %}
-
-
-
+
+
+
+
-{% endif %}
-
-
{{layout.header.title}}
-
{{layout.header.subtitle}}
-
\ No newline at end of file
+
+
{{layout.header.title}}
+
{{layout.header.subtitle}}
+
\ 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
-
-
-
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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"
-__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
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 @@
{% else %}
-
+
{% endif %}
{{_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
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 @@
{% endif %}
{{_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
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):
"""
_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/")
+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
-
+
{%include "header.html" %}
From b283d26717b9304e0263a5df565ab2da1738fa16 Mon Sep 17 00:00:00 2001
From: Steve Nyemba
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
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(['
'.replace(':id',id),_html,'
'])
- _html = ' '.join(['
'.replace(':id',id),_html,'
'])
- 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(['
'.replace(':id',id),_html,'
'])
+# _html = ' '.join(['
'.replace(':id',id),_html,'
'])
+# 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('//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.route("/",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/')
# 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//api//")
-def _delegate_call(app,module,name):
- global _route
- _handler = _route._apps[app]
+
+@_app.route("/api//",defaults={'app':'main','key':None})
+@_app.route("//api//",defaults={'key':None})
+@_app.route("////",defaults={'key':None})
+
+def _delegate_call(app,key,module,name):
+ _handler = _getHandler(app,key)
return _delegate(_handler,module,name)
-@_app.route('/api//')
-def _api(module,name) :
+# @_app.route('/api//')
+@_app.route("///api//", methods=['GET'])
+@_app.route("/api//",defaults={'app_id':'main','key':None})
+@_app.route("//api//",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//" , methods=['POST'])
-def _post (module,name):
+
+# @_app.route("/api//" , methods=['POST'],defaults={'app_id':'main','key':None})
+# @_app.route('//api//',methods=['POST'],defaults={'key':None})
+# @_app.route('///api//',methods=['POST'],defaults={'app_id':'main','key':None})
+
+@_app.route("///api//", methods=['POST'])
+@_app.route("/api//",defaults={'app_id':'main','key':None},methods=['POST'])
+@_app.route("//api//",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('//page',methods=['POST'],defaults={'key':None})
+@_app.route('///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('//page',defaults={'resource':None},methods=['GET'])
+@_app.route('///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/')
-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('/')
-def _open(id):
- global _route
- # _handler = _route.get()
+# @_app.route('/set/')
+# 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('/')
+# 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 @@
-
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
-
-
-
-
+
+
+
+
@@ -44,16 +44,16 @@ Vanderbilt University Medical Center
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+