mirror of http://localhost:9400/cloud/cms
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			368 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			368 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Python
		
	
#!/usr/bin/env python
 | 
						|
import numpy as np
 | 
						|
import os
 | 
						|
import sys
 | 
						|
import json
 | 
						|
import importlib
 | 
						|
import importlib.util
 | 
						|
# from git.repo.base import Repo
 | 
						|
import typer
 | 
						|
from typing_extensions import Annotated
 | 
						|
from typing import Optional
 | 
						|
from typing import Tuple
 | 
						|
 | 
						|
 | 
						|
import meta
 | 
						|
import uuid
 | 
						|
from termcolor import colored
 | 
						|
import base64
 | 
						|
import io
 | 
						|
import cms
 | 
						|
from cms import index
 | 
						|
 | 
						|
from cms.engine.config.structure import Layout, System
 | 
						|
from cms.engine import project, config, themes, plugins
 | 
						|
import pandas as pd
 | 
						|
import requests
 | 
						|
 | 
						|
 | 
						|
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'),']'])
 | 
						|
 | 
						|
INVALID_FOLDER = """
 | 
						|
{FAILED} Unable to proceed, could not find project manifest. It should be qcms-manifest.json
 | 
						|
"""
 | 
						|
#
 | 
						|
# handling cli interface
 | 
						|
cli = typer.Typer()
 | 
						|
 | 
						|
def get_manifest (manifest):
 | 
						|
    if not manifest.endswith('json') and os.path.isdir(manifest):
 | 
						|
        manifest = manifest if not manifest.endswith(os.sep) else os.sep.join(manifest.split(os.sep)[:-1])
 | 
						|
        return os.sep.join([manifest,'qcms-manifest.json'])
 | 
						|
    else:
 | 
						|
        return manifest
 | 
						|
 | 
						|
 | 
						|
@cli.command(name="info")
 | 
						|
def _info():
 | 
						|
    """
 | 
						|
    This function returns metadata information about this program, no parameters are needed
 | 
						|
    """
 | 
						|
    print ()
 | 
						|
    print ('QCMS',meta.__version__)
 | 
						|
    print (meta.__author__,meta.__email__)
 | 
						|
    print ()
 | 
						|
 | 
						|
@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):
 | 
						|
    """
 | 
						|
    Setup application access i.e port, debug, and/or context
 | 
						|
    These parameters are the same used in any flask application
 | 
						|
 | 
						|
    """
 | 
						|
    global INVALID_FOLDER
 | 
						|
    _config = config.get()
 | 
						|
    if _config :
 | 
						|
        _app = _config['system']['app']
 | 
						|
        _app['host'] = host
 | 
						|
        _app['port'] = port
 | 
						|
        _app['debug'] = debug
 | 
						|
        _config['system']['context'] = context
 | 
						|
        _config['app'] = _app
 | 
						|
        config.write(_config)
 | 
						|
        _msg = f"""{PASSED} Successful update, good job !
 | 
						|
        """
 | 
						|
    else:
 | 
						|
        _msg = INVALID_FOLDER
 | 
						|
    print (_msg)
 | 
						|
@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):
 | 
						|
    """
 | 
						|
    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)
 | 
						|
        _auth = json.loads(f.read())
 | 
						|
        if not set(['url','token','uid'])  -  set(_auth.keys()) :
 | 
						|
            url = _auth['url']
 | 
						|
 | 
						|
            if os.path.exists('qcms-manifest.json') :
 | 
						|
                _config = config.get()
 | 
						|
                _config['system']['source'] = path #{'id':'cloud','auth':{'url':url,'uid':uid,'token':token}}
 | 
						|
                config.write(_config)
 | 
						|
                title = _config['layout']['header']['title']
 | 
						|
                _msg = f"""{PASSED} Successfully update, good job!
 | 
						|
                    {url} 
 | 
						|
                """
 | 
						|
            else:
 | 
						|
                _msg = INVALID_FOLDER
 | 
						|
        else:
 | 
						|
            _msg = """NOT A VALID NEXTCLOUD FILE"""
 | 
						|
    else:
 | 
						|
        _msg = INVALID_FOLDER
 | 
						|
    print (_msg)
 | 
						|
@cli.command(name='secure')
 | 
						|
# def set_key(path):
 | 
						|
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 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):
 | 
						|
        f = open(keyfile,'w')
 | 
						|
        f.write(str(uuid.uuid4()))
 | 
						|
        f.close()
 | 
						|
        #
 | 
						|
        _config = config.get(manifest)
 | 
						|
        if 'source' not in _config['system']:
 | 
						|
            _config['system']['source'] = {'id':'disk'}
 | 
						|
        _config['system']['source']['key'] = keyfile
 | 
						|
        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 ...`
 | 
						|
        """
 | 
						|
    else:
 | 
						|
        _msg = f"""{FAILED} Could NOT generate a key, because it would seem you already have one
 | 
						|
            Please manually delete {keyfile}
 | 
						|
 | 
						|
            
 | 
						|
        """
 | 
						|
    print (_msg)
 | 
						|
 | 
						|
def load(**_args):
 | 
						|
    """
 | 
						|
    This function will load external module form a given location and return a pointer to a function in a given module
 | 
						|
    :path   absolute path of the file (considered plugin) to be loaded
 | 
						|
    :name   name of the function to be applied
 | 
						|
    """
 | 
						|
    _path = _args['path'] #os.sep.join([_args['root'],'plugin'])
 | 
						|
    if os.path.isdir(_path):
 | 
						|
        files = os.listdir(_path)
 | 
						|
        if files :
 | 
						|
            files = [name for name in files if name.endswith('.py')]
 | 
						|
            if files:
 | 
						|
                _path = os.sep.join([_path,files[0]])
 | 
						|
            else:
 | 
						|
                return None
 | 
						|
        else:
 | 
						|
            return None
 | 
						|
    #-- We have a file ...  
 | 
						|
    _name = _args['name']
 | 
						|
    spec = importlib.util.spec_from_file_location(_name, _path)
 | 
						|
    module = importlib.util.module_from_spec(spec)
 | 
						|
    spec.loader.exec_module(module)
 | 
						|
    
 | 
						|
    return getattr(module,_name) if hasattr(module,_name) else None
 | 
						|
 | 
						|
@cli.command(name='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,
 | 
						|
    """
 | 
						|
    manifest = get_manifest(manifest)
 | 
						|
    _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)
 | 
						|
                _data = cms.Plugin.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)
 | 
						|
            _fnpointer = cms.Plugin.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:Annotated[str,typer.Argument(help="path of the project folder")],
 | 
						|
        #    root:Annotated[str,typer.Argument(help="folder of the content (inside the project folder)")],
 | 
						|
           index:str=typer.Option(default="index.html",help="index file (markup or html)"), #Annotated[str,typer.Argument(help="index file to load (.html or markup)")]='index.html',
 | 
						|
           title:str=typer.Option(default="QCMS PROJECT", help="name of the project") , #Annotated[str,typer.Argument(help="title of the project")]='QCMS - TITLE',
 | 
						|
           subtitle:str=typer.Option(default="designed & built by The Phi Technology",help="tag line about the project (sub-title)"), #Annotated[str,typer.Argument(help="subtitle of the project")]='qcms - subtitle',
 | 
						|
           footer:str=typer.Option(default="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'
 | 
						|
           ):
 | 
						|
    """
 | 
						|
    Create a project folder by performing a git clone (pull the template project)
 | 
						|
    and adding a configuration file to that will bootup the project
 | 
						|
    """
 | 
						|
 | 
						|
    #
 | 
						|
    # Build the configuration for the project
 | 
						|
    if not os.path.exists(folder) :
 | 
						|
        root = 'www/html'
 | 
						|
        _system = System()
 | 
						|
        _layout = Layout()
 | 
						|
        _layout = _layout.build(root=root, order={'menu':[]}, footer = [{"text":term} for term in footer.split(',')],header={'title':title,'subtitle':subtitle} )
 | 
						|
        
 | 
						|
        _system = System()
 | 
						|
        _system = _system.build(version=version,port=port, context=context)
 | 
						|
        _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
 | 
						|
        """)
 | 
						|
 | 
						|
 | 
						|
@cli.command (name='reload')
 | 
						|
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( get_manifest(path))
 | 
						|
    if 'source' in _config['system'] and '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 to request reload"""
 | 
						|
    print (_msg)
 | 
						|
@cli.command(name="bootup")
 | 
						|
def bootup (
 | 
						|
    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
 | 
						|
    """
 | 
						|
    # if not manifest.endswith('json') and os.path.isdir(manifest):
 | 
						|
    #     manifest = manifest if not manifest.endswith(os.sep) else os.sep.join(manifest.split(os.sep)[:-1])
 | 
						|
    #     manifest = os.sep.join([manifest,'qcms-manifest.json'])
 | 
						|
    manifest = get_manifest(manifest)
 | 
						|
    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
 | 
						|
    
 | 
						|
    """
 | 
						|
    manifest = get_manifest(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.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 and not show:
 | 
						|
        # 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()
 |