diff --git a/cms/cloud.py b/cms/cloud.py new file mode 100644 index 0000000..a2e0ecf --- /dev/null +++ b/cms/cloud.py @@ -0,0 +1,112 @@ +""" +Reads from nextcloud +""" + +import nextcloud_client as nc +import copy +from mistune import markdown + + +_CLOUDHANDLER = None +def _format_root_folder (_root): + if _root[0] == '/' : + _root = _root[1:] + if _root[-1] == '/' : + _root = _root[:-1] + return _root +def content(_args): + """ + :url + :uid + :token + :folder + """ + global _CLOUDHANDLER + _handler = nc.Client(_args['url']) + _handler.login(_args['uid'],_args['token']) + _CLOUDHANDLER = _handler + _files = _handler.list(_args['folder'],10) + _root = _args['folder'] + if _root.startswith('/') : + _root = _root[1:] + if _root.endswith('/') : + _root = _root[:-1] + _menu = {} #[_args['folder']] + [_item for _item in _files if _item.file_type == 'dir' and _item.name[0] not in ['.','_']] + _menu = {} #dict.fromkeys(_menu,[]) + for _item in _files : + _folder = _item.path.split(_item.name)[0].strip() + _folder = _folder.replace(_root,'').replace('/','') + + if _item.name[0] in ['.','_'] or _folder == '': + continue ; + + if _item.file_type == 'file' and _item.get_content_type() in ['text/markdown','text/html'] : + # _folder = _item.path.split(_item.name)[0].strip() + # _folder = _folder.replace(_root,'').replace('//','') + if _folder == '' : + _folder = str(_root) + _folder = _folder.replace('/' ,' ').strip() + if _folder not in _menu : + _menu [_folder] = [] + # print ([_item.name,_key, _key in _menu]) + + # _menuItem = _ref[_key] + # uri = '/'.join([_args['url'],_item.path]) + # uri = _item + # print ([_menuItem, _menuItem in _menu]) + uri = '/'.join(_item.path.split('/')[2:]) + _menu[_folder].append({'text':_item.name.split('.')[0],'uri':uri}) + # + # clean up the content ... + _keys = [_name for _name in _menu.keys() if _name.startswith('_')] + [_menu.pop(_name) for _name in _keys] + return _menu + + +def build(_config): + """ + The function will build a menu based on a folder structure in nextcloud + :_args authentication arguments for nextcloud + :_config configuration for the cms + """ + _args = copy.deepcopy(_config['system']['source']['auth']) + _args['folder'] = _config['layout']['root'] + # update(_config) + return content(_args) +def html (uri,_config) : + global _CLOUDHANDLER + _handler = _CLOUDHANDLER + _root = _format_root_folder(_config['layout']['root']) + uri = _format_root_folder (uri) + + + _prefix = '/'.join (uri.split('/')[:-1]) + _link = '/'.join(['{{context}}api/cloud/download?doc='+_prefix,'.attachments.']) + # _link = '/'.join(['api/cloud/download?doc='+_prefix,'_images']) + _html = _handler.get_file_contents(uri).decode('utf-8').replace('.attachments.',_link) + # print ([uri,uri[-2:] ,uri[-2:] in ['md','MD','markdown']]) + return markdown(_html) if uri[-2:] in ['md','MD','Md','mD'] else _html.replace(_root,('{{context}}api/cloud/download?doc='+_root)) +# def update (_config): +# """ +# This function updates the configuration provided by loading default plugins +# """ +# if 'plugins' not in _config : +# _config['plugins'] = {} +# _config['plugins'] = plugins () +# return _config +def download(**_args): + _handler = _CLOUDHANDLER + + if _args['doc'][-2:] in ['md','ht']: + _stream = html(_args['doc'],_args['config']) + else: + _stream = _handler.get_file_contents(_args['doc']) + + return _stream + pass + +def plugins (): + """ + This function publishes the plugins associated with this module + """ + return {'api/cloud/download':download} diff --git a/cms/disk.py b/cms/disk.py new file mode 100644 index 0000000..89c8526 --- /dev/null +++ b/cms/disk.py @@ -0,0 +1,40 @@ +""" +This file pulls the content from the disk +""" +import os +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('_')] + +def content(_folder): + """ + :content of the folder + """ + + if os.path.exists(_folder) : + _menuItems = os.listdir(_folder) + # return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_folder,_name])} for _name in os.listdir(_folder) if not _name.startswith('_') and os.path.isfile( os.sep.join([_folder,_name]))] + return [{'text':_name.split('.')[0].replace('_', ' ').replace('-',' ').strip(),'uri': os.sep.join([_folder,_name])} for _name in os.listdir(_folder) if not _name.startswith('_') and os.path.isfile( os.sep.join([_folder,_name]))] + else: + return [] +def build (_config): #(_path,_content): + """ + building the menu for the site given the content is on disk + :path path of the files on disk + :config configuration associated with the + """ + _path = _config['layout']['root'] + _items = folders(_path) + _subItems = [ content (os.sep.join([_path,_name]))for _name in _items ] + # return dict(zip()) + return dict.fromkeys(_items,_subItems) + +def html(uri) : + _html = (open(uri)).read() + return _html +def plugins (): + return {} + \ No newline at end of file diff --git a/cms/plugins.py b/cms/plugins.py new file mode 100644 index 0000000..8819807 --- /dev/null +++ b/cms/plugins.py @@ -0,0 +1,16 @@ +""" +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 +""" + + + + +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/static/css/themes/default.css b/static/css/themes/default.css new file mode 100644 index 0000000..ce61daa --- /dev/null +++ b/static/css/themes/default.css @@ -0,0 +1,5 @@ +/** +* This is the default window and we will have to hide the pane (side) +*/ + +body .pane { display:none} \ No newline at end of file diff --git a/static/css/themes/magazine.css b/static/css/themes/magazine.css new file mode 100644 index 0000000..9a4d869 --- /dev/null +++ b/static/css/themes/magazine.css @@ -0,0 +1,90 @@ +.main { + margin:10px; + padding:4px; + display:grid; + grid-template-columns: 50% 50% ; gap:4px; + grid-template-rows: 48px 48px auto 32px; + font-family: helvetica; + font-weight: lighter; + font-size:18px; + line-height: 1.5; + justify-items: normal; + ; +} +.main .header { + height:48px; + grid-row:1; + grid-column: 1 / span 2; + + +} + +.main .header img { width:40px; margin:4px;} +.main .menu { grid-row:2; grid-column: 1 / span 2; background-color: #f3f3f3; } +.main .content { + grid-row:3; + grid-column: 1 ; + text-wrap: wrap; + height:100%; + display:grid; + align-content: start; + + + +} +.main .content #index { + text-align: left; + text-wrap: wrap; +} +/* .main .content #index{ + text-align:left; + align-content:normal; + display:grid; + background-color: green; +} */ + +.main .pane { + border-left:3px dotted gray; + grid-column: 2; + font-family: sans-serif; + + +} +.pane iframe { + border:1px solid transparent; + width:99%; + height:100%; + +} +.main .footer {grid-row:4; grid-column: 1 / span 2; font-size:13px; font-weight: lighter;} + +/** +* styling tables here +*/ +p { + margin-top:22px; +} +table { + width:99%; + border: 1px solid #CAD5E0; +} +table td {padding:4px; margin:4px;} +table thead { + + font-weight:bold; + background-color:#f3f3f3; +} +/* table tbody tr:hover { + background-color: #4682B4; + color:white ; + cursor:pointer; +} */ + +#gallery img { + justify-content: left; + width:70px; + + +} + +#gallery table {width:100%;}