mirror of http://localhost:9400/cloud/cms
				
				
				
			
							parent
							
								
									cbf63d3f56
								
							
						
					
					
						commit
						d9bf641a24
					
				@ -0,0 +1,352 @@
 | 
				
			|||||||
 | 
					"""
 | 
				
			||||||
 | 
					This file will describe how a site is loaded
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					from cms.engine.config.structure import Layout, System
 | 
				
			||||||
 | 
					from cms import disk, cloud
 | 
				
			||||||
 | 
					import copy
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					import io
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					from jinja2 import Environment, BaseLoader, FileSystemLoader
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IOConfig:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    This class encapsulates read/write for configuration
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    def __init__(self,**_args):
 | 
				
			||||||
 | 
					        self._config = {'system':{},'layout':{},'plugins':{}}
 | 
				
			||||||
 | 
					        self._caller = None
 | 
				
			||||||
 | 
					        self._location= _args['location'] if 'location' in _args else None       
 | 
				
			||||||
 | 
					        self._logs = []
 | 
				
			||||||
 | 
					    def get(self,_key) :
 | 
				
			||||||
 | 
					        if not _key :
 | 
				
			||||||
 | 
					            return self._config
 | 
				
			||||||
 | 
					        _keys = _key.split('.') if '.' in _key else [_key]
 | 
				
			||||||
 | 
					        _value= None
 | 
				
			||||||
 | 
					        _object = copy.deepcopy(self._config)
 | 
				
			||||||
 | 
					        for _key in _keys :
 | 
				
			||||||
 | 
					            if _key in _object :
 | 
				
			||||||
 | 
					                _object = _object[_key]
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                return None
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        return _object
 | 
				
			||||||
 | 
					    def set(self,_expr,value):
 | 
				
			||||||
 | 
					        if len(_expr.split('.')) == 1 :
 | 
				
			||||||
 | 
					            self._config[_expr] = value
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            _object = self._config
 | 
				
			||||||
 | 
					            for _attr in _expr.split('.')[:-1] : 
 | 
				
			||||||
 | 
					                    if _attr not in _object :
 | 
				
			||||||
 | 
					                        _object[_attr] = {}
 | 
				
			||||||
 | 
					                    _object = _object[_attr]
 | 
				
			||||||
 | 
					            _attr = _expr.split('.')[-1]
 | 
				
			||||||
 | 
					            _object[_attr] = value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def log(self,**_args):
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        _row = dict({'_date':datetime.now().strftime('%Y-%m-%d'),'time':datetime.now().strftime('%H:%M:%S'), 'context':self.get('system.context')},**_args)
 | 
				
			||||||
 | 
					        self._logs.append(_row)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    def debug(self,**_args):
 | 
				
			||||||
 | 
					        return self._logs,'application/json'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Initialization (IOConfig):
 | 
				
			||||||
 | 
					    def __init__(self,**_args):
 | 
				
			||||||
 | 
					        super().__init__(**_args)
 | 
				
			||||||
 | 
					        self._config = self.read_config(**_args)
 | 
				
			||||||
 | 
					        self._args = _args
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # Invoke initialization
 | 
				
			||||||
 | 
					        self.reload()
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    def reload (self):
 | 
				
			||||||
 | 
					        _args  = self._args
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self._caller = _args['caller'] if 'caller' in _args else None
 | 
				
			||||||
 | 
					        if (self._caller):
 | 
				
			||||||
 | 
					            self._config['system']['parentContext'] = ''
 | 
				
			||||||
 | 
					        if not self.get('system.icon') :
 | 
				
			||||||
 | 
					            self._config['system']['icon'] = None
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self._config['layout']['location'] = None
 | 
				
			||||||
 | 
					        if not self.get('layout.menu') :
 | 
				
			||||||
 | 
					            self._config['layout']['menu'] = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # self.context(**_args)
 | 
				
			||||||
 | 
					        self.locations(**_args)
 | 
				
			||||||
 | 
					        self.menu()
 | 
				
			||||||
 | 
					        self.plugins()        
 | 
				
			||||||
 | 
					    def read_config(self,**_args) :
 | 
				
			||||||
 | 
					        _config = {}
 | 
				
			||||||
 | 
					        if 'path' in _args :
 | 
				
			||||||
 | 
					            f = open(_args['path'])
 | 
				
			||||||
 | 
					            _config = json.loads(f.read())
 | 
				
			||||||
 | 
					            f.close()
 | 
				
			||||||
 | 
					        elif 'stream' in _args :
 | 
				
			||||||
 | 
					            _stream = _args['stream']
 | 
				
			||||||
 | 
					            if type(_stream) == 'str' :
 | 
				
			||||||
 | 
					                _config  = json.loads(_stream)
 | 
				
			||||||
 | 
					            elif type(_stream) == io.StringIO :
 | 
				
			||||||
 | 
					                _config =  json.loads( _stream.read())
 | 
				
			||||||
 | 
					        _name = self._caller.__name__ if self._caller else None
 | 
				
			||||||
 | 
					        self.log(action='init.config',module='read_config',input={'caller':_name})
 | 
				
			||||||
 | 
					        return _config
 | 
				
			||||||
 | 
					    def context(self,**_args):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Updating the context so that we can have a consistent representation
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if ('context' in _args):           
 | 
				
			||||||
 | 
					            self.set('system.context',_args['context'])
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        _context = self.get('system.context')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self._caller :
 | 
				
			||||||
 | 
					            #
 | 
				
			||||||
 | 
					            # There is a parent context we need to account and we are updating the current context to reflect the caller context
 | 
				
			||||||
 | 
					            _parentContext  = self._caller.get('system.context')
 | 
				
			||||||
 | 
					            _context = '/'.join([self._caller.get('system.context'),_context])
 | 
				
			||||||
 | 
					            #
 | 
				
			||||||
 | 
					            # updating the configuratioin
 | 
				
			||||||
 | 
					            _iconURI = '/'.join(["",_parentContext,self._caller.get('system.icon')])
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            # print ([self._caller.get('system.context'), _parentContext,_context])
 | 
				
			||||||
 | 
					            if self._caller.get('system.context') != '':
 | 
				
			||||||
 | 
					                _parentContext = "/"+_parentContext
 | 
				
			||||||
 | 
					                _context = "/"+_context
 | 
				
			||||||
 | 
					                self.set('system.onport',0)
 | 
				
			||||||
 | 
					                self._config['system']['onport'] = 0
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                _iconURI = _iconURI.replace("///","/")
 | 
				
			||||||
 | 
					                self.set('system.onport',1)
 | 
				
			||||||
 | 
					                self._config['system']['onport'] = 1
 | 
				
			||||||
 | 
					            self.set('system.caller',{'icon':_iconURI,'context':self._caller.get('system.context')})
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					                # _context = _context.replace('/','')
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            #
 | 
				
			||||||
 | 
					            # If we have no parent site, there should be no parent context (or empty string)
 | 
				
			||||||
 | 
					            _parentContext = _context #@TODO: should be none ?
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # Updating context so it is usable throughout the code base
 | 
				
			||||||
 | 
					        if self._caller:
 | 
				
			||||||
 | 
					            self.set('system.parentContext', _parentContext.strip())
 | 
				
			||||||
 | 
					        self.set('system.context',_context.strip())
 | 
				
			||||||
 | 
					        # self._config['system']['context'] = _context.strip()
 | 
				
			||||||
 | 
					        self._config['system']['parentContext'] = _parentContext
 | 
				
			||||||
 | 
					        p = {'has_caller':self._caller != None,'context':_context}
 | 
				
			||||||
 | 
					        self.log(action='init.context',module='context',input=p)
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # loosly context to a certain extent involves locations of icons and document root
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        self.locations(**_args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # submit log of status 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def locations(self,**_args):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        We are updating the icons, project location file and security key (if any), we need to make sure that context is updated first
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # updating location & path of the application
 | 
				
			||||||
 | 
					        # if self.get('system.caller') :
 | 
				
			||||||
 | 
					        #     return 
 | 
				
			||||||
 | 
					        _context = self.get('system.context')
 | 
				
			||||||
 | 
					        _logo = self.get('system.logo')
 | 
				
			||||||
 | 
					        _root = self.get('layout.root')
 | 
				
			||||||
 | 
					        if self.get('system.source.id') == 'cloud' :
 | 
				
			||||||
 | 
					            _icon = f'{_context}/api/cloud/download?doc={_logo}'
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            _icon = f'{_context}/api/disk/read?uri={_logo}'
 | 
				
			||||||
 | 
					            # _root = f'{_context}/api/disk/read?uri={_root}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # self.set('layout.root',_root)
 | 
				
			||||||
 | 
					        self.set('system.icon',_icon)
 | 
				
			||||||
 | 
					        self.set('system.logo',_icon)
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # from the path provided we can determine the location of the project
 | 
				
			||||||
 | 
					        _homefolder = os.sep.join(_args['path'].split(os.sep)[:-1])
 | 
				
			||||||
 | 
					        if _homefolder and os.path.exists(os.sep.join([_homefolder,_root])) :
 | 
				
			||||||
 | 
					            self.set('layout.location',_homefolder)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # updating the security key and trying to determine the location 
 | 
				
			||||||
 | 
					        if self.get('system.source.key') :
 | 
				
			||||||
 | 
					            _path = self.get('system.source.key')
 | 
				
			||||||
 | 
					            f = None
 | 
				
			||||||
 | 
					            if os.path.exists(_path) :
 | 
				
			||||||
 | 
					                f = open(_path)
 | 
				
			||||||
 | 
					            elif os.path.exists(os.sep.join([self.get('layout.location'),_path])) :
 | 
				
			||||||
 | 
					                f  = open(os.sep.join([self.get('layout.location'),_path]))
 | 
				
			||||||
 | 
					            if f :
 | 
				
			||||||
 | 
					                self.set('system.source.key',f.read())
 | 
				
			||||||
 | 
					                f.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def menu (self,**_args):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        This function will build the menu of the site (if need be)
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        _handler = cloud if self.get('system.source.id') == 'cloud' else disk
 | 
				
			||||||
 | 
					        _object = _handler.build(self._config)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        _overwrite = self.get('layout.overwrite')
 | 
				
			||||||
 | 
					        for _name in _object :
 | 
				
			||||||
 | 
					            _submenu = _object[_name]
 | 
				
			||||||
 | 
					            _index = 0
 | 
				
			||||||
 | 
					            for _item in _submenu :
 | 
				
			||||||
 | 
					                _text = _item['text'].strip()
 | 
				
			||||||
 | 
					                if _text in _overwrite :
 | 
				
			||||||
 | 
					                    if 'uri' in _item and 'url' in _overwrite :
 | 
				
			||||||
 | 
					                        del _item['uri']
 | 
				
			||||||
 | 
					                    _item = dict(_item,**_overwrite[_text])
 | 
				
			||||||
 | 
					                _submenu[_index] = _item
 | 
				
			||||||
 | 
					                _index += 1
 | 
				
			||||||
 | 
					        self.set('layout.menu',_object)
 | 
				
			||||||
 | 
					        # self._config['layout']['menu'] = _object
 | 
				
			||||||
 | 
					        self.routes()   #-- other apps/sites integrated in case we are operating in portal mode
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # At this point the entire menu is build and we need to have it sorted
 | 
				
			||||||
 | 
					        self.order()
 | 
				
			||||||
 | 
					        # _labels = list(self.get('layout.menu').keys())
 | 
				
			||||||
 | 
					        # self.log(action='init.menu',module='menu',input=_labels)
 | 
				
			||||||
 | 
					        # print (self.get('layout.menu'))
 | 
				
			||||||
 | 
					        # print (_object)
 | 
				
			||||||
 | 
					    def order(self,**_args):
 | 
				
			||||||
 | 
					        if self.get('layout.order.menu') :
 | 
				
			||||||
 | 
					            _sorted = {}
 | 
				
			||||||
 | 
					            _menu = self.get('layout.menu')
 | 
				
			||||||
 | 
					            _ordered = self.get('layout.order.menu')
 | 
				
			||||||
 | 
					            for _name in _ordered :
 | 
				
			||||||
 | 
					                if _name in _menu :
 | 
				
			||||||
 | 
					                    _sorted[_name] = _menu[_name]
 | 
				
			||||||
 | 
					            if _sorted :
 | 
				
			||||||
 | 
					                _menu = _sorted
 | 
				
			||||||
 | 
					            _missing = list(set(_menu.keys()) - set(_sorted))
 | 
				
			||||||
 | 
					            if _missing :
 | 
				
			||||||
 | 
					                for _name in _missing :
 | 
				
			||||||
 | 
					                    _menu[_name] = _menu[_name]
 | 
				
			||||||
 | 
					        self.set('layout.menu',_menu)
 | 
				
			||||||
 | 
					    def routes (self,**_args):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        This method will update menu with dependent sites/apps and add them to the menu
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        _overwrite  = self.get('layout.overwrite')
 | 
				
			||||||
 | 
					        _routes     = self.get('system.routes')
 | 
				
			||||||
 | 
					        _context    = self.get('system.context')
 | 
				
			||||||
 | 
					        _menu       = self.get('layout.menu')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if _routes :
 | 
				
			||||||
 | 
					            for _text in _routes :
 | 
				
			||||||
 | 
					                _item = _routes[_text]
 | 
				
			||||||
 | 
					                if 'menu' in _item :
 | 
				
			||||||
 | 
					                    _uri = f'{_context}/{_text}'
 | 
				
			||||||
 | 
					                    _label = _item['menu']
 | 
				
			||||||
 | 
					                    if _text in _overwrite :
 | 
				
			||||||
 | 
					                        _text = _overwrite[_text]['text']
 | 
				
			||||||
 | 
					                    if _label not in _menu :
 | 
				
			||||||
 | 
					                        _menu[_label] = []
 | 
				
			||||||
 | 
					                    _menu[_label].append({"text":_text,"uri":_uri,"type":"open"})
 | 
				
			||||||
 | 
					            #
 | 
				
			||||||
 | 
					            # updating menu ...
 | 
				
			||||||
 | 
					            self.set('layout.menu',_menu)
 | 
				
			||||||
 | 
					            # self._config['layout']['menu'] = _menu
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					    def plugins (self,**_args):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        This function will load the plugins from disk (only)
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        _folder     = os.sep.join([self.get('layout.location'),self.get('layout.root'),'_plugins'])
 | 
				
			||||||
 | 
					        _context    = self.get('system.context')
 | 
				
			||||||
 | 
					        _parentContext = self.get('system.parentContext')
 | 
				
			||||||
 | 
					        _map        = {}
 | 
				
			||||||
 | 
					        _plugins = {}
 | 
				
			||||||
 | 
					        if self.get('system.source.id') == 'cloud' :
 | 
				
			||||||
 | 
					            _plugins = cloud.plugins(_context)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            _plugins = disk.plugins(context=_context)
 | 
				
			||||||
 | 
					        _uri = 'api/system/debug'
 | 
				
			||||||
 | 
					        _uri = _uri if not _context else f'{_context}/{_uri}'
 | 
				
			||||||
 | 
					        _plugins[_uri] = self.debug
 | 
				
			||||||
 | 
					        if os.path.exists(_folder) and self.get('plugins'):
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            _items = self.get('plugins') 
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for _filename in _items:
 | 
				
			||||||
 | 
					                _path = os.sep.join([_folder,_filename+'.py'])
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if os.path.exists(_path) :
 | 
				
			||||||
 | 
					                    for _module in _items[_filename] :
 | 
				
			||||||
 | 
					                        _pointer = disk.plugins(path=_path,name=_module,context=_context)
 | 
				
			||||||
 | 
					                        if _pointer :
 | 
				
			||||||
 | 
					                            _uri = f"api/{_filename}/{_module}"
 | 
				
			||||||
 | 
					                            _uri = f"{_context}/{_uri}" if _context else _uri
 | 
				
			||||||
 | 
					                            _map[_uri] = _pointer
 | 
				
			||||||
 | 
					                            if _parentContext :
 | 
				
			||||||
 | 
					                                # _uri = f"{_parentContext}/{_context}"
 | 
				
			||||||
 | 
					                                _map[_uri] = _pointer
 | 
				
			||||||
 | 
					                            
 | 
				
			||||||
 | 
					                            self.log(action='load.plugin',module='plugins',input={'uri':_uri,'path':_path,'name':_module})
 | 
				
			||||||
 | 
					                        else:
 | 
				
			||||||
 | 
					                            self.log(action='missing.function',module='plugins',input={'name':_module,'path':_path})
 | 
				
			||||||
 | 
					                        
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    #
 | 
				
			||||||
 | 
					                    # log the error ...
 | 
				
			||||||
 | 
					                    self.log(action='missing.plugins',module='plugins',input=_path)
 | 
				
			||||||
 | 
					            #
 | 
				
			||||||
 | 
					            # Updating plugins from disk/cloud
 | 
				
			||||||
 | 
					            _plugins = _map if not _plugins else dict(_plugins,**_map)
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        self.set('plugins',_plugins)
 | 
				
			||||||
 | 
					        self.log(action='init.plugins',module='plugins',input=list(_plugins.keys()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Site(Initialization) :
 | 
				
			||||||
 | 
					    def __init__(self,**_args):
 | 
				
			||||||
 | 
					        super().__init__(**_args)
 | 
				
			||||||
 | 
					        self._config['system']['portal'] = (self.get('system.routes')) == None
 | 
				
			||||||
 | 
					    def html(self,_uri,_id) :
 | 
				
			||||||
 | 
					        _handler = cloud if self.get('system.source.id') == 'cloud' else disk
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        _html = _handler.html(_uri, self.get(None))
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return " ".join([f'<div id="{_id}"> ',_html,"</div>"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class QCMS:
 | 
				
			||||||
 | 
					    def __init__(self,**_args):
 | 
				
			||||||
 | 
					        _app = Site(**_args)
 | 
				
			||||||
 | 
					        self._id = _app.get('system.context') #if _app.get('system.context') else 'main'
 | 
				
			||||||
 | 
					        self._sites = {self._id:_app}
 | 
				
			||||||
 | 
					        if _app.get('system.routes') :
 | 
				
			||||||
 | 
					            _routes = _app.get('system.routes')
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for _name in _routes :
 | 
				
			||||||
 | 
					                _path = _routes[_name]['path']
 | 
				
			||||||
 | 
					                self._sites[_name] = Site(context=_name,path=_path,caller=_app)
 | 
				
			||||||
 | 
					    def render(self,_uri,_id,_appid=None):
 | 
				
			||||||
 | 
					        _site = self._sites[_appid] if _appid else self._sites[self._id]
 | 
				
			||||||
 | 
					        _args = {'layout':_site.get('layout')}
 | 
				
			||||||
 | 
					        _system = _site.get('system')
 | 
				
			||||||
 | 
					        for k in ['source','app'] :
 | 
				
			||||||
 | 
					            if k in _system :
 | 
				
			||||||
 | 
					                del _system[k]
 | 
				
			||||||
 | 
					        _args['system'] = _system
 | 
				
			||||||
 | 
					        _html = _site.html(_uri,_id)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        _env = Environment(loader=BaseLoader()).from_string(_html)
 | 
				
			||||||
 | 
					        _args[_id] = str(_env.render(**_args))
 | 
				
			||||||
 | 
					        return _args
 | 
				
			||||||
 | 
					    def set(self,_id):
 | 
				
			||||||
 | 
					        self._id = _id
 | 
				
			||||||
 | 
					    def get(self,_id=None):
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        return self._sites[self._id] if not _id else self._sites[_id]
 | 
				
			||||||
					Loading…
					
					
				
		Reference in new issue