mirror of
https://github.com/yweber/lodel2.git
synced 2025-11-12 17:09:16 +01:00
Moved plugins folder in lodel/
- updated scripts etc - make tests pass
This commit is contained in:
parent
250f4b8683
commit
0406e91846
82 changed files with 28 additions and 8 deletions
|
|
@ -1,9 +0,0 @@
|
|||
plugins_PYTHON=__init__.py
|
||||
|
||||
pluginsdir=$(pkgpythondir)/plugins
|
||||
|
||||
install-data-local:
|
||||
mkdir ${DESTDIR}$(pluginsdir); cp -R * ${DESTDIR}$(pluginsdir) && rm ${DESTDIR}$(pluginsdir)/Makefile*
|
||||
|
||||
uninstall-hook:
|
||||
-rm -R ${DESTDIR}$(pluginsdir)
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
##@defgroup lodel2_plugins_list Plugins lodel
|
||||
#@brief Regroup all implemented plugin documentation
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.settings.validator': ['SettingValidator']})
|
||||
|
||||
__plugin_name__ = "dummy"
|
||||
__version__ = '0.0.1' #or __version__ = [0,0,1]
|
||||
__loader__ = "main.py"
|
||||
__confspec__ = "confspec.py"
|
||||
__author__ = "Lodel2 dev team"
|
||||
__fullname__ = "Dummy plugin"
|
||||
__name__ = 'yweber.dummy'
|
||||
__plugin_type__ = 'extension'
|
||||
|
||||
|
||||
##@brief This methods allow plugin writter to write some checks
|
||||
#
|
||||
#@return True if checks are OK else return a string with a reason
|
||||
def _activate():
|
||||
import leapi_dyncode
|
||||
print("Testing dynamic objects : ")
|
||||
print("Object : ", leapi_dyncode.Object)
|
||||
print("Publication : ", leapi_dyncode.Publication)
|
||||
print("Publication fields : ", leapi_dyncode.Publication.fieldnames())
|
||||
return True
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.settings.validator': ['SettingValidator']})
|
||||
|
||||
CONFSPEC = {
|
||||
'lodel2.section1': {
|
||||
'key1': ( None,
|
||||
SettingValidator('dummy'))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.plugin': ['LodelHook', 'CustomMethod']})
|
||||
|
||||
@LodelHook('leapi_get_post')
|
||||
@LodelHook('leapi_update_pre')
|
||||
@LodelHook('leapi_update_post')
|
||||
@LodelHook('leapi_delete_pre')
|
||||
@LodelHook('leapi_delete_post')
|
||||
@LodelHook('leapi_insert_pre')
|
||||
@LodelHook('leapi_insert_post')
|
||||
def dummy_callback(hook_name, caller, payload):
|
||||
if Lodel.settings.Settings.debug:
|
||||
print("\tHook %s\tcaller %s with %s" % (hook_name, caller, payload))
|
||||
return payload
|
||||
|
||||
|
||||
@CustomMethod('Object', 'dummy_method')
|
||||
def dummy_instance_method(self):
|
||||
print("Hello world !\
|
||||
I'm a custom method on an instance of class %s" % self.__class__)
|
||||
|
||||
|
||||
@CustomMethod('Object', 'dummy_class_method', CustomMethod.CLASS_METHOD)
|
||||
def dummy_instance_method(self):
|
||||
print("Hello world !\
|
||||
I'm a custom method on class %s" % self.__class__)
|
||||
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.settings.validator': ['SettingValidator']})
|
||||
from .datasource import DummyDatasource as Datasource
|
||||
|
||||
__plugin_type__ = 'datasource'
|
||||
__plugin_name__ = "dummy_datasource"
|
||||
__version__ = '0.0.1'
|
||||
__loader__ = 'main.py'
|
||||
__plugin_deps__ = []
|
||||
|
||||
CONFSPEC = {
|
||||
'lodel2.datasource.dummy_datasource.*' : {
|
||||
'dummy': ( None,
|
||||
SettingValidator('dummy'))}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
#-*- coding:utf-8 -*-
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.plugin.datasource_plugin': ['AbstractDatasource']})
|
||||
|
||||
class DummyDatasource(AbstractDatasource):
|
||||
|
||||
def __init__(self, *conn_args, **conn_kwargs):
|
||||
self.conn_args = conn_args
|
||||
self.conn_kwargs = conn_kwargs
|
||||
|
||||
##@brief Provide a new uniq numeric ID
|
||||
#@param emcomp LeObject subclass (not instance) : To know on wich things we
|
||||
#have to be uniq
|
||||
#@return an integer
|
||||
def new_numeric_id(self, emcomp):
|
||||
pass
|
||||
|
||||
##@brief returns a selection of documents from the datasource
|
||||
#@param target_cls Emclass
|
||||
#@param field_list list
|
||||
#@param filters list : List of filters
|
||||
#@param rel_filters list : List of relational filters
|
||||
#@param order list : List of column to order. ex: order = [('title', 'ASC'),]
|
||||
#@param group list : List of tupple representing the column to group together. ex: group = [('title', 'ASC'),]
|
||||
#@param limit int : Number of records to be returned
|
||||
#@param offset int: used with limit to choose the start record
|
||||
#@param instanciate bool : If true, the records are returned as instances, else they are returned as dict
|
||||
#@return list
|
||||
def select(self, target, field_list, filters, relational_filters=None, order=None, group=None, limit=None, offset=0,
|
||||
instanciate=True):
|
||||
pass
|
||||
|
||||
##@brief Deletes records according to given filters
|
||||
#@param target Emclass : class of the record to delete
|
||||
#@param filters list : List of filters
|
||||
#@param relational_filters list : List of relational filters
|
||||
#@return int : number of deleted records
|
||||
def delete(self, target, filters, relational_filters):
|
||||
return 0
|
||||
|
||||
## @brief updates records according to given filters
|
||||
#@param target Emclass : class of the object to insert
|
||||
#@param filters list : List of filters
|
||||
#@param relational_filters list : List of relational filters
|
||||
#@param upd_datas dict : datas to update (new values)
|
||||
#@return int : Number of updated records
|
||||
def update(self, target, filters, relational_filters, upd_datas):
|
||||
return 0
|
||||
|
||||
## @brief Inserts a record in a given collection
|
||||
# @param target Emclass : class of the object to insert
|
||||
# @param new_datas dict : datas to insert
|
||||
# @return the inserted uid
|
||||
def insert(self, target, new_datas):
|
||||
return 0
|
||||
|
||||
## @brief Inserts a list of records in a given collection
|
||||
# @param target Emclass : class of the objects inserted
|
||||
# @param datas_list list : list of dict
|
||||
# @return list : list of the inserted records' ids
|
||||
def insert_multi(self, target, datas_list):
|
||||
return 0
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
#-*- coding:utf-8 -*-
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.plugin': ['LodelHook']})
|
||||
from .datasource import DummyDatasource as Datasource
|
||||
|
||||
def migration_handler_class():
|
||||
from .migration_handler import DummyMigrationHandler as migration_handler
|
||||
return migration_handler
|
||||
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
##@brief Abtract class for migration handlers
|
||||
class DummyMigrationHandler(object):
|
||||
|
||||
##@brief Create a new migration handler given DB connection options
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
##@brief DB initialisation
|
||||
#@param emclass_list list : list of EmClasses concerned by this MH
|
||||
def init_db(self, emclass_list):
|
||||
pass
|
||||
|
||||
##@todo redefine
|
||||
def register_change(self, model, uid, initial_state, new_state):
|
||||
pass
|
||||
|
||||
##@todo redefine
|
||||
def register_model_state(self, em, state_hash):
|
||||
pass
|
||||
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.settings.validator': ['SettingValidator']})
|
||||
|
||||
__plugin_name__ = 'filesystem_session'
|
||||
__version__ = [0,0,1]
|
||||
__plugin_type__ = 'session_handler'
|
||||
__loader__ = 'main.py'
|
||||
__confspec__ = "confspec.py"
|
||||
__author__ = "Lodel2 dev team"
|
||||
__fullname__ = "FileSystem Session Store Plugin"
|
||||
|
||||
|
||||
## @brief This methods allow plugin writter to write some checks
|
||||
#
|
||||
# @return True if checks are OK else returns a string with a reason
|
||||
def _activate():
|
||||
return True
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.settings.validator': ['SettingValidator']})
|
||||
|
||||
CONFSPEC = {
|
||||
'lodel2.sessions':{
|
||||
'directory': ('/tmp/', SettingValidator('path')),
|
||||
'expiration': (900, SettingValidator('int')),
|
||||
'file_template': ('lodel2_%s.sess', SettingValidator('dummy'))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
## @brief An extended dictionary representing a session in the file system
|
||||
class FileSystemSession(dict):
|
||||
|
||||
## @brief Constructor
|
||||
# @param token str
|
||||
def __init__(self, token):
|
||||
self.__token = token
|
||||
self.__path = None
|
||||
|
||||
## @brief token getter
|
||||
# @return str
|
||||
@property
|
||||
def token(self):
|
||||
return self.__token
|
||||
|
||||
## @brief path getter
|
||||
# @return str
|
||||
@property
|
||||
def path(self):
|
||||
return self.__path
|
||||
|
||||
## @brief path setter
|
||||
# @param path str
|
||||
@path.setter
|
||||
def path(self, path):
|
||||
self.__path = path
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import binascii
|
||||
import datetime
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
import time
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.logger': 'logger',
|
||||
'lodel.auth.exceptions': ['ClientAuthenticationFailure'],
|
||||
'lodel.settings': ['Settings']})
|
||||
|
||||
from .filesystem_session import FileSystemSession
|
||||
|
||||
__sessions = dict()
|
||||
|
||||
SESSION_TOKENSIZE = 150
|
||||
|
||||
|
||||
## @brief generates a new session token
|
||||
# @return str
|
||||
def generate_token():
|
||||
token = binascii.hexlify(os.urandom(SESSION_TOKENSIZE//2))
|
||||
if token in __sessions.keys():
|
||||
token = generate_token()
|
||||
return token.decode('utf-8')
|
||||
|
||||
|
||||
## @brief checks the validity of a given session token
|
||||
# @param token str
|
||||
# @raise ClientAuthenticationFailure for invalid or not found session token
|
||||
def check_token(token):
|
||||
if len(token) != SESSION_TOKENSIZE:
|
||||
raise ClientAuthenticationFailure("Invalid token string")
|
||||
if token not in __sessions.keys():
|
||||
raise ClientAuthenticationFailure("No session found for this token")
|
||||
|
||||
## @brief returns a session file path for a specific token
|
||||
def generate_file_path(token):
|
||||
return os.path.abspath(os.path.join(Settings.sessions.directory, Settings.sessions.file_template) % token)
|
||||
|
||||
|
||||
def get_token_from_filepath(filepath):
|
||||
token_regex = re.compile(os.path.abspath(os.path.join(Settings.sessions.directory, Settings.sessions.file_template % '(?P<token>.*)')))
|
||||
token_search_result = token_regex.match(filepath)
|
||||
if token_search_result is not None:
|
||||
return token_search_result.groupdict()['token']
|
||||
return None
|
||||
|
||||
|
||||
## @brief returns the session's last modification timestamp
|
||||
# @param token str
|
||||
# @return float
|
||||
# @raise ValueError if the given token doesn't match with an existing session
|
||||
def get_session_last_modified(token):
|
||||
if token in __sessions[token]:
|
||||
return os.stat(__sessions[token]).st_mtime
|
||||
else:
|
||||
raise ValueError("The given token %s doesn't match with an existing session")
|
||||
|
||||
|
||||
## @brief returns the token of a new session
|
||||
# @return str
|
||||
def start_session():
|
||||
session = FileSystemSession(generate_token())
|
||||
session.path = generate_file_path(session.token)
|
||||
with open(session.path, 'wb') as session_file:
|
||||
pickle.dump(session, session_file)
|
||||
__sessions[session.token] = session.path
|
||||
logger.debug("New session created")
|
||||
return session.token
|
||||
|
||||
|
||||
## @brief destroys a session given its token
|
||||
# @param token str
|
||||
def destroy_session(token):
|
||||
check_token(token)
|
||||
if os.path.isfile(__sessions[token]):
|
||||
os.unlink(__sessions[token])
|
||||
logger.debug("Session file for %s destroyed" % token)
|
||||
del(__sessions[token])
|
||||
logger.debug("Session %s unregistered" % token)
|
||||
|
||||
|
||||
## @brief restores a session's content
|
||||
# @param token str
|
||||
# @return FileSystemSession|None
|
||||
def restore_session(token):
|
||||
gc()
|
||||
check_token(token)
|
||||
logger.debug("Restoring session : %s" % token)
|
||||
if os.path.isfile(__sessions[token]):
|
||||
with open(__sessions[token], 'rb') as session_file:
|
||||
session = pickle.load(session_file)
|
||||
return session
|
||||
else:
|
||||
return None # raise FileNotFoundError("Session file not found for the token %s" % token)
|
||||
|
||||
|
||||
## @brief saves the session's content to a file
|
||||
# @param token str
|
||||
# @param datas dict
|
||||
def save_session(token, datas):
|
||||
session = datas
|
||||
if not isinstance(datas, FileSystemSession):
|
||||
session = FileSystemSession(token)
|
||||
session.path = generate_file_path(token)
|
||||
session.update(datas)
|
||||
|
||||
with open(__sessions[token], 'wb') as session_file:
|
||||
pickle.dump(session, session_file)
|
||||
|
||||
if token not in __sessions.keys():
|
||||
__sessions[token] = session.path
|
||||
|
||||
logger.debug("Session %s saved" % token)
|
||||
|
||||
|
||||
## @brief session store's garbage collector
|
||||
def gc():
|
||||
# Unregistered files in the session directory
|
||||
session_files_directory = os.path.abspath(Settings.sessions.directory)
|
||||
for session_file in [file_path for file_path in os.listdir(session_files_directory) if os.path.isfile(os.path.join(session_files_directory, file_path))]:
|
||||
session_file_path = os.path.join(session_files_directory, session_file)
|
||||
token = get_token_from_filepath(session_file_path)
|
||||
if token is None or token not in __sessions.keys():
|
||||
os.unlink(session_file_path)
|
||||
logger.debug("Unregistered session file %s has been deleted" % session_file)
|
||||
|
||||
# Expired registered sessions
|
||||
for token in __sessions.keys():
|
||||
if os.path.isfile(__sessions[token]):
|
||||
now_timestamp = time.mktime(datetime.datetime.now().timetuple())
|
||||
if now_timestamp - get_session_last_modified(token) > Settings.sessions.expiration:
|
||||
destroy_session(token)
|
||||
logger.debug("Expired session %s has been destroyed" % token)
|
||||
|
||||
|
||||
def set_session_value(token, key, value):
|
||||
session = restore_session(token)
|
||||
session[key] = value
|
||||
save_session(token, session)
|
||||
|
||||
|
||||
def get_session_value(token, key):
|
||||
session = restore_session(token)
|
||||
return session[key]
|
||||
|
||||
|
||||
def del_session_value(token, key):
|
||||
session = restore_session(token)
|
||||
if key in session:
|
||||
del(session[key])
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
__plugin_type__ = 'datasource'
|
||||
__plugin_name__ = 'mongodb_datasource'
|
||||
__version__ = '0.0.1'
|
||||
__plugin_type__ = 'datasource'
|
||||
|
||||
__loader__ = "main.py"
|
||||
__confspec__ = "confspec.py"
|
||||
|
||||
__author__ = "Lodel2 dev team"
|
||||
__fullname__ = "MongoDB plugin"
|
||||
|
||||
|
||||
## @brief Activates the plugin
|
||||
#
|
||||
# @note It is possible there to add some specific actions (like checks, etc ...) for the plugin
|
||||
#
|
||||
# @return bool|str : True if all the checks are OK, an error message if not
|
||||
def _activate():
|
||||
from lodel import buildconf #NOTE : this one do not have to pass through the context
|
||||
return buildconf.PYMONGO
|
||||
|
||||
#
|
||||
# Doxygen comments
|
||||
#
|
||||
|
||||
##@defgroup plugin_mongodb_datasource MongoDB datasource plugin
|
||||
#@brief Doc about mongodb datasource
|
||||
|
||||
##@page plugin_mongodb_backref_complexity Reflexion on back reference complexity
|
||||
#@ingroup plugin_mongodb_bref_op
|
||||
#
|
||||
#Their is a huge performance issue in the way we implemented references
|
||||
#and back references for mongodb datasource :
|
||||
#
|
||||
#For each write action (update, delete or insert) we HAVE TO run a select
|
||||
#on all concerned LeObject. In fact those methods headers looks like
|
||||
#<pre>def write_action(target_cls, filters, [datas])</pre>
|
||||
#
|
||||
#We have no idea if all the modified objects are of the target class (they
|
||||
#can be of any target's child classes). So that means we have no idea of the
|
||||
#@ref base_classes.Reference "References" that will be modified by the action.
|
||||
#
|
||||
#Another problem is that when we run an update or a delete we have no idea
|
||||
#of the values that will be updated or deleted (we do not have the concerned
|
||||
#instances datas). As a result we cannot replace or delete the concerned
|
||||
#back references.
|
||||
#
|
||||
#In term of complexity the number of DB query looks like :
|
||||
#<pre>
|
||||
#With n the number of instances to modify :
|
||||
#queryO(n) ~= 2n ( n * select + n * update )
|
||||
#</pre>
|
||||
#But it can go really bad, really fast if we take in consideration that
|
||||
#query's can be done on mixed classes or abstract classes. With :
|
||||
#- n : the number of LeObect child classes represented by the abstract class
|
||||
#- m : the number of LeObject child classes for each n
|
||||
#- o : the number of concerned back_reference classes for each m
|
||||
#
|
||||
#<pre>queryO(n,m,o) ~= n + (n*m) + (n*m*o) => n + n*m select and n*m*o updates</pre>
|
||||
#
|
||||
#All of this is really sad especially as the update and the delete will be
|
||||
#run on LeObject instances....
|
||||
#
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.settings.validator': ['SettingValidator']})
|
||||
|
||||
##@brief Mongodb datasource plugin confspec
|
||||
#@ingroup plugin_mongodb_datasource
|
||||
#
|
||||
#Describe mongodb plugin configuration. Keys are :
|
||||
CONFSPEC = {
|
||||
'lodel2.datasource.mongodb_datasource.*':{
|
||||
'read_only': (False, SettingValidator('bool')),
|
||||
'host': ('localhost', SettingValidator('host')),
|
||||
'port': (None, SettingValidator('string', none_is_valid = True)),
|
||||
'db_name':('lodel', SettingValidator('string')),
|
||||
'username': (None, SettingValidator('string')),
|
||||
'password': (None, SettingValidator('string'))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,872 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
import warnings
|
||||
import copy
|
||||
import functools
|
||||
from bson.son import SON
|
||||
from collections import OrderedDict
|
||||
import pymongo
|
||||
from pymongo.errors import BulkWriteError
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.logger': 'logger',
|
||||
'lodel.leapi.leobject': ['CLASS_ID_FIELDNAME'],
|
||||
'lodel.leapi.datahandlers.base_classes': ['Reference', 'MultipleRef'],
|
||||
'lodel.exceptions': ['LodelException', 'LodelFatalError'],
|
||||
'lodel.plugin.datasource_plugin': ['AbstractDatasource']})
|
||||
|
||||
from . import utils
|
||||
from .exceptions import *
|
||||
from .utils import object_collection_name, collection_name, \
|
||||
MONGODB_SORT_OPERATORS_MAP, connection_string, mongo_fieldname
|
||||
|
||||
|
||||
##@brief Datasource class
|
||||
#@ingroup plugin_mongodb_datasource
|
||||
class MongoDbDatasource(AbstractDatasource):
|
||||
|
||||
##@brief Stores existing connections
|
||||
#
|
||||
#The key of this dict is a hash of the connection string + ro parameter.
|
||||
#The value is a dict with 2 keys :
|
||||
# - conn_count : the number of instanciated datasource that use this
|
||||
#connection
|
||||
# - db : the pymongo database object instance
|
||||
_connections = dict()
|
||||
|
||||
##@brief Mapping from lodel2 operators to mongodb operator
|
||||
lodel2mongo_op_map = {
|
||||
'=':'$eq', '<=':'$lte', '>=':'$gte', '!=':'$ne', '<':'$lt',
|
||||
'>':'$gt', 'in':'$in', 'not in':'$nin' }
|
||||
##@brief List of mongodb operators that expect re as value
|
||||
mongo_op_re = ['$in', '$nin']
|
||||
wildcard_re = re.compile('[^\\\\]\*')
|
||||
|
||||
##@brief instanciates a database object given a connection name
|
||||
#@param host str : hostname or IP
|
||||
#@param port int : mongodb listening port
|
||||
#@param db_name str
|
||||
#@param username str
|
||||
#@param password str
|
||||
#@param read_only bool : If True the Datasource is for read only, else the
|
||||
#Datasource is write only !
|
||||
def __init__(self, host, port, db_name, username, password, read_only = False):
|
||||
##@brief Connections infos that can be kept securly
|
||||
self.__db_infos = {'host': host, 'port': port, 'db_name': db_name}
|
||||
##@brief Is the instance read only ? (if not it's write only)
|
||||
self.__read_only = bool(read_only)
|
||||
##@brief Uniq ID for mongodb connection
|
||||
self.__conn_hash= None
|
||||
##@brief Stores the database cursor
|
||||
self.database = self.__connect(
|
||||
username, password, db_name, self.__read_only)
|
||||
|
||||
##@brief Destructor that attempt to close connection to DB
|
||||
#
|
||||
#Decrease the conn_count of associated MongoDbDatasource::_connections
|
||||
#item. If it reach 0 close the connection to the db
|
||||
#@see MongoDbDatasource::__connect()
|
||||
def __del__(self):
|
||||
self._connections[self.__conn_hash]['conn_count'] -= 1
|
||||
if self._connections[self.__conn_hash]['conn_count'] <= 0:
|
||||
self._connections[self.__conn_hash]['db'].close()
|
||||
del(self._connections[self.__conn_hash])
|
||||
logger.info("Closing connection to database")
|
||||
|
||||
##@brief Provide a new uniq numeric ID
|
||||
#@param emcomp LeObject subclass (not instance) : To know on wich things we
|
||||
#have to be uniq
|
||||
#@warning multiple UID broken by this method
|
||||
#@return an integer
|
||||
def new_numeric_id(self, emcomp):
|
||||
target = emcomp.uid_source()
|
||||
tuid = target._uid[0] # Multiple UID broken here
|
||||
results = self.select(
|
||||
target, field_list = [tuid], filters = [],
|
||||
order=[(tuid, 'DESC')], limit = 1)
|
||||
if len(results) == 0:
|
||||
return 1
|
||||
return results[0][tuid]+1
|
||||
|
||||
##@brief returns a selection of documents from the datasource
|
||||
#@param target Emclass
|
||||
#@param field_list list
|
||||
#@param filters list : List of filters
|
||||
#@param relational_filters list : List of relational filters
|
||||
#@param order list : List of column to order. ex: order =
|
||||
#[('title', 'ASC'),]
|
||||
#@param group list : List of tupple representing the column used as
|
||||
#"group by" fields. ex: group = [('title', 'ASC'),]
|
||||
#@param limit int : Number of records to be returned
|
||||
#@param offset int: used with limit to choose the start record
|
||||
#@return list
|
||||
#@todo Implement group for abstract LeObject childs
|
||||
def select(self, target, field_list, filters = None,
|
||||
relational_filters=None, order=None, group=None, limit=None,
|
||||
offset=0):
|
||||
if target.is_abstract():
|
||||
#Reccursiv calls for abstract LeObject child
|
||||
results = self.__act_on_abstract(target, filters,
|
||||
relational_filters, self.select, field_list = field_list,
|
||||
order = order, group = group, limit = limit)
|
||||
|
||||
#Here we may implement the group
|
||||
#If sorted query we have to sort again
|
||||
if order is not None:
|
||||
results = sorted(results,
|
||||
key=functools.cmp_to_key(
|
||||
self.__generate_lambda_cmp_order(order)))
|
||||
#If limit given apply limit again
|
||||
if offset > len(results):
|
||||
results = list()
|
||||
else:
|
||||
if limit is not None:
|
||||
if limit + offset > len(results):
|
||||
limit = len(results)-offset-1
|
||||
results = results[offset:offset+limit]
|
||||
return results
|
||||
# Default behavior
|
||||
if filters is None:
|
||||
filters = list()
|
||||
if relational_filters is None:
|
||||
relational_filters = list()
|
||||
|
||||
collection_name = object_collection_name(target)
|
||||
collection = self.database[collection_name]
|
||||
|
||||
query_filters = self.__process_filters(
|
||||
target, filters, relational_filters)
|
||||
|
||||
query_result_ordering = None
|
||||
if order is not None:
|
||||
query_result_ordering = utils.parse_query_order(order)
|
||||
|
||||
if group is None:
|
||||
if field_list is None:
|
||||
field_list = dict()
|
||||
else:
|
||||
f_list=dict()
|
||||
for fl in field_list:
|
||||
f_list[fl] = 1
|
||||
field_list = f_list
|
||||
field_list['_id'] = 0
|
||||
cursor = collection.find(
|
||||
spec = query_filters,
|
||||
fields=field_list,
|
||||
skip=offset,
|
||||
limit=limit if limit != None else 0,
|
||||
sort=query_result_ordering)
|
||||
else:
|
||||
pipeline = list()
|
||||
unwinding_list = list()
|
||||
grouping_dict = OrderedDict()
|
||||
sorting_list = list()
|
||||
for group_param in group:
|
||||
field_name = group_param[0]
|
||||
field_sort_option = group_param[1]
|
||||
sort_option = MONGODB_SORT_OPERATORS_MAP[field_sort_option]
|
||||
unwinding_list.append({'$unwind': '$%s' % field_name})
|
||||
grouping_dict[field_name] = '$%s' % field_name
|
||||
sorting_list.append((field_name, sort_option))
|
||||
|
||||
sorting_list.extends(query_result_ordering)
|
||||
|
||||
pipeline.append({'$match': query_filters})
|
||||
if field_list is not None:
|
||||
pipeline.append({
|
||||
'$project': SON([{field_name: 1}
|
||||
for field_name in field_list])})
|
||||
pipeline.extend(unwinding_list)
|
||||
pipeline.append({'$group': grouping_dict})
|
||||
pipeline.extend({'$sort': SON(sorting_list)})
|
||||
if offset > 0:
|
||||
pipeline.append({'$skip': offset})
|
||||
if limit is not None:
|
||||
pipeline.append({'$limit': limit})
|
||||
|
||||
results = list()
|
||||
for document in cursor:
|
||||
results.append(document)
|
||||
|
||||
return results
|
||||
|
||||
##@brief Deletes records according to given filters
|
||||
#@param target Emclass : class of the record to delete
|
||||
#@param filters list : List of filters
|
||||
#@param relational_filters list : List of relational filters
|
||||
#@return int : number of deleted records
|
||||
def delete(self, target, filters, relational_filters):
|
||||
if target.is_abstract():
|
||||
logger.debug("Delete called on %s filtered by (%s,%s). Target is \
|
||||
abstract, preparing reccursiv calls" % (target, filters, relational_filters))
|
||||
#Deletion with abstract LeObject as target (reccursiv calls)
|
||||
return self.__act_on_abstract(target, filters,
|
||||
relational_filters, self.delete)
|
||||
logger.debug("Delete called on %s filtered by (%s,%s)." % (
|
||||
target, filters, relational_filters))
|
||||
#Non abstract beahavior
|
||||
mongo_filters = self.__process_filters(
|
||||
target, filters, relational_filters)
|
||||
#Updating backref before deletion
|
||||
self.__update_backref_filtered(target, filters, relational_filters,
|
||||
None)
|
||||
res = self.__collection(target).remove(mongo_filters)
|
||||
return res['n']
|
||||
|
||||
##@brief updates records according to given filters
|
||||
#@param target Emclass : class of the object to insert
|
||||
#@param filters list : List of filters
|
||||
#@param relational_filters list : List of relational filters
|
||||
#@param upd_datas dict : datas to update (new values)
|
||||
#@return int : Number of updated records
|
||||
def update(self, target, filters, relational_filters, upd_datas):
|
||||
self._data_cast(upd_datas)
|
||||
#fetching current datas state
|
||||
mongo_filters = self.__process_filters(
|
||||
target, filters, relational_filters)
|
||||
old_datas_l = self.__collection(target).find(
|
||||
mongo_filters)
|
||||
old_datas_l = list(old_datas_l)
|
||||
#Running update
|
||||
res = self.__update_no_backref(target, filters, relational_filters,
|
||||
upd_datas)
|
||||
#updating backref
|
||||
self.__update_backref_filtered(target, filters, relational_filters,
|
||||
upd_datas, old_datas_l)
|
||||
return res
|
||||
|
||||
##@brief Designed to be called by backref update in order to avoid
|
||||
#infinite updates between back references
|
||||
#@see update()
|
||||
def __update_no_backref(self, target, filters, relational_filters,
|
||||
upd_datas):
|
||||
logger.debug("Update called on %s filtered by (%s,%s) with datas \
|
||||
%s" % (target, filters, relational_filters, upd_datas))
|
||||
if target.is_abstract():
|
||||
#Update using abstract LeObject as target (reccursiv calls)
|
||||
return self.__act_on_abstract(target, filters,
|
||||
relational_filters, self.update, upd_datas = upd_datas)
|
||||
#Non abstract beahavior
|
||||
mongo_filters = self.__process_filters(
|
||||
target, filters, relational_filters)
|
||||
self._data_cast(upd_datas)
|
||||
mongo_arg = {'$set': upd_datas }
|
||||
res = self.__collection(target).update(mongo_filters, mongo_arg)
|
||||
return res['n']
|
||||
|
||||
## @brief Inserts a record in a given collection
|
||||
# @param target Emclass : class of the object to insert
|
||||
# @param new_datas dict : datas to insert
|
||||
# @return the inserted uid
|
||||
def insert(self, target, new_datas):
|
||||
self._data_cast(new_datas)
|
||||
logger.debug("Insert called on %s with datas : %s"% (
|
||||
target, new_datas))
|
||||
uidname = target.uid_fieldname()[0] #MULTIPLE UID BROKEN HERE
|
||||
if uidname not in new_datas:
|
||||
raise MongoDataSourceError("Missing UID data will inserting a new \
|
||||
%s" % target.__class__)
|
||||
res = self.__collection(target).insert(new_datas)
|
||||
self.__update_backref(target, new_datas[uidname], None, new_datas)
|
||||
return str(res)
|
||||
|
||||
## @brief Inserts a list of records in a given collection
|
||||
# @param target Emclass : class of the objects inserted
|
||||
# @param datas_list list : list of dict
|
||||
# @return list : list of the inserted records' ids
|
||||
def insert_multi(self, target, datas_list):
|
||||
for datas in datas_list:
|
||||
self._data_cast(datas)
|
||||
res = self.__collection(target).insert_many(datas_list)
|
||||
for new_datas in datas_list:
|
||||
self.__update_backref(target, None, new_datas)
|
||||
target.make_consistency(datas=new_datas)
|
||||
return list(res.inserted_ids)
|
||||
|
||||
##@brief Update backref giving an action
|
||||
#@param target leObject child class
|
||||
#@param filters
|
||||
#@param relational_filters,
|
||||
#@param new_datas None | dict : optional new datas if None mean we are deleting
|
||||
#@param old_datas_l None | list : if None fetch old datas from db (usefull
|
||||
#when modifications are made on instance before updating backrefs)
|
||||
#@return nothing (for the moment
|
||||
def __update_backref_filtered(self, target,
|
||||
filters, relational_filters, new_datas = None, old_datas_l = None):
|
||||
#Getting all the UID of the object that will be deleted in order
|
||||
#to update back_references
|
||||
if old_datas_l is None:
|
||||
mongo_filters = self.__process_filters(
|
||||
target, filters, relational_filters)
|
||||
old_datas_l = self.__collection(target).find(
|
||||
mongo_filters)
|
||||
old_datas_l = list(old_datas_l)
|
||||
|
||||
uidname = target.uid_fieldname()[0] #MULTIPLE UID BROKEN HERE
|
||||
for old_datas in old_datas_l:
|
||||
self.__update_backref(
|
||||
target, old_datas[uidname], old_datas, new_datas)
|
||||
|
||||
##@brief Update back references of an object
|
||||
#@ingroup plugin_mongodb_bref_op
|
||||
#
|
||||
#old_datas and new_datas arguments are set to None to indicate
|
||||
#insertion or deletion. Calls examples :
|
||||
#@par LeObject insert __update backref call
|
||||
#<pre>
|
||||
#Insert(datas):
|
||||
# self.make_insert(datas)
|
||||
# self.__update_backref(self.__class__, None, datas)
|
||||
#</pre>
|
||||
#@par LeObject delete __update backref call
|
||||
#Delete()
|
||||
# old_datas = self.datas()
|
||||
# self.make_delete()
|
||||
# self.__update_backref(self.__class__, old_datas, None)
|
||||
#@par LeObject update __update_backref call
|
||||
#<pre>
|
||||
#Update(new_datas):
|
||||
# old_datas = self.datas()
|
||||
# self.make_udpdate(new_datas)
|
||||
# self.__update_backref(self.__class__, old_datas, new_datas)
|
||||
#</pre>
|
||||
#
|
||||
#@param target LeObject child classa
|
||||
#@param tuid mixed : The target UID (the value that will be inserted in
|
||||
#back references)
|
||||
#@param old_datas dict : datas state before update
|
||||
#@param new_datas dict : datas state after the update process
|
||||
#retun None
|
||||
def __update_backref(self, target, tuid, old_datas, new_datas):
|
||||
#upd_dict is the dict that will allow to run updates in an optimized
|
||||
#way (or try to help doing it)
|
||||
#
|
||||
#It's struct looks like :
|
||||
# { LeoCLASS : {
|
||||
# UID1: (
|
||||
# LeoINSTANCE,
|
||||
# { fname1 : value, fname2: value }),
|
||||
# UID2 (LeoINSTANCE, {fname...}),
|
||||
# },
|
||||
# LeoClass2: {...
|
||||
#
|
||||
upd_dict = {}
|
||||
for fname, fdh in target.reference_handlers().items():
|
||||
oldd = old_datas is not None and fname in old_datas and \
|
||||
(not hasattr(fdh, 'default') or old_datas[fname] != fdh.default) \
|
||||
and not old_datas[fname] is None
|
||||
newd = new_datas is not None and fname in new_datas and \
|
||||
(not hasattr(fdh, 'default') or new_datas[fname] != fdh.default) \
|
||||
and not new_datas[fname] is None
|
||||
if (oldd and newd and old_datas[fname] == new_datas[fname])\
|
||||
or not(oldd or newd):
|
||||
#No changes or not concerned
|
||||
continue
|
||||
bref_cls = fdh.back_reference[0]
|
||||
bref_fname = fdh.back_reference[1]
|
||||
if issubclass(fdh.__class__, MultipleRef):
|
||||
#fdh is a multiple ref. So the update preparation will be
|
||||
#divided into two loops :
|
||||
#- one loop for deleting old datas
|
||||
#- one loop for inserting updated datas
|
||||
#
|
||||
#Preparing the list of values to delete or to add
|
||||
if newd and oldd:
|
||||
old_values = old_datas[fname]
|
||||
new_values = new_datas[fname]
|
||||
to_del = [ val
|
||||
for val in old_values
|
||||
if val not in new_values]
|
||||
to_add = [ val
|
||||
for val in new_values
|
||||
if val not in old_values]
|
||||
elif oldd and not newd:
|
||||
to_del = old_datas[fname]
|
||||
to_add = []
|
||||
elif not oldd and newd:
|
||||
to_del = []
|
||||
to_add = new_datas[fname]
|
||||
#Calling __back_ref_upd_one_value() with good arguments
|
||||
for vtype, vlist in [('old',to_del), ('new', to_add)]:
|
||||
for value in vlist:
|
||||
#fetching backref infos
|
||||
bref_infos = self.__bref_get_check(
|
||||
bref_cls, value, bref_fname)
|
||||
#preparing the upd_dict
|
||||
upd_dict = self.__update_backref_upd_dict_prepare(
|
||||
upd_dict, bref_infos, bref_fname, value)
|
||||
#preparing updated bref_infos
|
||||
bref_cls, bref_leo, bref_dh, bref_value = bref_infos
|
||||
bref_infos = (bref_cls, bref_leo, bref_dh,
|
||||
upd_dict[bref_cls][value][1][bref_fname])
|
||||
vdict = {vtype: value}
|
||||
#fetch and store updated value
|
||||
new_bref_val = self.__back_ref_upd_one_value(
|
||||
fname, fdh, tuid, bref_infos, **vdict)
|
||||
upd_dict[bref_cls][value][1][bref_fname] = new_bref_val
|
||||
else:
|
||||
#fdh is a single ref so the process is simpler, we do not have
|
||||
#to loop and we may do an update in only one
|
||||
#__back_ref_upd_one_value() call by giving both old and new
|
||||
#value
|
||||
vdict = {}
|
||||
if oldd:
|
||||
vdict['old'] = old_datas[fname]
|
||||
uid_val = vdict['old']
|
||||
if newd:
|
||||
vdict['new'] = new_datas[fname]
|
||||
if not oldd:
|
||||
uid_val = vdict['new']
|
||||
#Fetching back ref infos
|
||||
bref_infos = self.__bref_get_check(
|
||||
bref_cls, uid_val, bref_fname)
|
||||
#prepare the upd_dict
|
||||
upd_dict = self.__update_backref_upd_dict_prepare(
|
||||
upd_dict, bref_infos, bref_fname, uid_val)
|
||||
#forging update bref_infos
|
||||
bref_cls, bref_leo, bref_dh, bref_value = bref_infos
|
||||
bref_infos = (bref_cls, bref_leo, bref_dh,
|
||||
upd_dict[bref_cls][uid_val][1][bref_fname])
|
||||
#fetche and store updated value
|
||||
new_bref_val = self.__back_ref_upd_one_value(
|
||||
fname, fdh, tuid, bref_infos, **vdict)
|
||||
upd_dict[bref_cls][uid_val][1][bref_fname] = new_bref_val
|
||||
#Now we've got our upd_dict ready.
|
||||
#running the updates
|
||||
for bref_cls, uid_dict in upd_dict.items():
|
||||
for uidval, (leo, datas) in uid_dict.items():
|
||||
#MULTIPLE UID BROKEN 2 LINES BELOW
|
||||
self.__update_no_backref(
|
||||
leo.__class__, [(leo.uid_fieldname()[0], '=', uidval)],
|
||||
[], datas)
|
||||
|
||||
##@brief Utility function designed to handle the upd_dict of
|
||||
#__update_backref()
|
||||
#
|
||||
#Basically checks if a key exists at some level, if not create it with
|
||||
#the good default value (in most case dict())
|
||||
#@param upd_dict dict : in & out args modified by reference
|
||||
#@param bref_infos tuple : as returned by __bref_get_check()
|
||||
#@param bref_fname str : name of the field in referenced class
|
||||
#@param uid_val mixed : the UID of the referenced object
|
||||
#@return the updated version of upd_dict
|
||||
@staticmethod
|
||||
def __update_backref_upd_dict_prepare(upd_dict,bref_infos, bref_fname,
|
||||
uid_val):
|
||||
bref_cls, bref_leo, bref_dh, bref_value = bref_infos
|
||||
if bref_cls not in upd_dict:
|
||||
upd_dict[bref_cls] = {}
|
||||
if uid_val not in upd_dict[bref_cls]:
|
||||
upd_dict[bref_cls][uid_val] = (bref_leo, {})
|
||||
if bref_fname not in upd_dict[bref_cls][uid_val]:
|
||||
upd_dict[bref_cls][uid_val][1][bref_fname] = bref_value
|
||||
return upd_dict
|
||||
|
||||
|
||||
##@brief Prepare a one value back reference update
|
||||
#@param fname str : the source Reference field name
|
||||
#@param fdh DataHandler : the source Reference DataHandler
|
||||
#@param tuid mixed : the uid of the Leo that make reference to the backref
|
||||
#@param bref_infos tuple : as returned by __bref_get_check() method
|
||||
#@param old mixed : (optional **values) the old value
|
||||
#@param new mixed : (optional **values) the new value
|
||||
#@return the new back reference field value
|
||||
def __back_ref_upd_one_value(self, fname, fdh, tuid, bref_infos, **values):
|
||||
bref_cls, bref_leo, bref_dh, bref_val = bref_infos
|
||||
oldd = 'old' in values
|
||||
newdd = 'new' in values
|
||||
if bref_val is None:
|
||||
bref_val = bref_dh.empty()
|
||||
if issubclass(bref_dh.__class__, MultipleRef):
|
||||
if oldd and newdd:
|
||||
if tuid not in bref_val:
|
||||
raise MongoDbConsistencyError("The value we want to \
|
||||
delete in this back reference update was not found in the back referenced \
|
||||
object : %s. Value was : '%s'" % (bref_leo, tuid))
|
||||
return bref_val
|
||||
elif oldd and not newdd:
|
||||
#deletion
|
||||
old_value = values['old']
|
||||
if tuid not in bref_val:
|
||||
raise MongoDbConsistencyError("The value we want to \
|
||||
delete in this back reference update was not found in the back referenced \
|
||||
object : %s. Value was : '%s'" % (bref_leo, tuid))
|
||||
if isinstance(bref_val, tuple):
|
||||
bref_val = set(bref_val)
|
||||
if isinstance(bref_val, set):
|
||||
bref_val -= set([tuid])
|
||||
else:
|
||||
del(bref_val[bref_val.index(tuid)])
|
||||
elif not oldd and newdd:
|
||||
if tuid in bref_val:
|
||||
raise MongoDbConsistencyError("The value we want to \
|
||||
add in this back reference update was found in the back referenced \
|
||||
object : %s. Value was : '%s'" % (bref_leo, tuid))
|
||||
if isinstance(bref_val, tuple):
|
||||
bref_val = set(bref_val)
|
||||
if isinstance(bref_val, set):
|
||||
bref_val |= set([tuid])
|
||||
else:
|
||||
bref_val.append(tuid)
|
||||
else:
|
||||
#Single value backref
|
||||
if oldd and newdd:
|
||||
if bref_val != tuid:
|
||||
raise MongoDbConsistencyError("The backreference doesn't \
|
||||
have expected value. Expected was %s but found %s in %s" % (
|
||||
tuid, bref_val, bref_leo))
|
||||
return bref_val
|
||||
elif oldd and not newdd:
|
||||
#deletion
|
||||
if not hasattr(bref_dh, "default"):
|
||||
raise MongoDbConsistencyError("Unable to delete a \
|
||||
value for a back reference update. The concerned field don't have a default \
|
||||
value : in %s field %s" % (bref_leo,fname))
|
||||
bref_val = getattr(bref_dh, "default")
|
||||
elif not oldd and newdd:
|
||||
bref_val = tuid
|
||||
return bref_val
|
||||
|
||||
##@brief Fetch back reference informations
|
||||
#@warning thank's to __update_backref_act() this method is useless
|
||||
#@param bref_cls LeObject child class : __back_reference[0]
|
||||
#@param uidv mixed : UID value (the content of the reference field)
|
||||
#@param bref_fname str : the name of the back_reference field
|
||||
#@return tuple(bref_class, bref_LeObect_instance, bref_datahandler,
|
||||
#bref_value)
|
||||
#@throw MongoDbConsistencyError when LeObject instance not found given
|
||||
#uidv
|
||||
#@throw LodelFatalError if the back reference field is not a Reference
|
||||
#subclass (major failure)
|
||||
def __bref_get_check(self, bref_cls, uidv, bref_fname):
|
||||
bref_leo = bref_cls.get_from_uid(uidv)
|
||||
if bref_leo is None:
|
||||
raise MongoDbConsistencyError("Unable to get the object we make \
|
||||
reference to : %s with uid = %s" % (bref_cls, repr(uidv)))
|
||||
bref_dh = bref_leo.data_handler(bref_fname)
|
||||
if not isinstance(bref_dh, Reference):
|
||||
raise LodelFatalError("Found a back reference field that \
|
||||
is not a reference : '%s' field '%s'" % (bref_leo, bref_fname))
|
||||
bref_val = bref_leo.data(bref_fname)
|
||||
return (bref_leo.__class__, bref_leo, bref_dh, bref_val)
|
||||
|
||||
##@brief Act on abstract LeObject child
|
||||
#
|
||||
#This method is designed to be called by insert, select and delete method
|
||||
#when they encounter an abtract class
|
||||
#@param target LeObject child class
|
||||
#@param filters
|
||||
#@param relational_filters
|
||||
#@param act function : the caller method
|
||||
#@param **kwargs other arguments
|
||||
#@return sum of results (if it's an array it will result in a concat)
|
||||
#@todo optimization implementing a cache for __bref_get_check()
|
||||
def __act_on_abstract(self,
|
||||
target, filters, relational_filters, act, **kwargs):
|
||||
|
||||
result = list() if act == self.select else 0
|
||||
if not target.is_abstract():
|
||||
target_childs = target
|
||||
else:
|
||||
target_childs = [tc for tc in target.child_classes()
|
||||
if not tc.is_abstract()]
|
||||
for target_child in target_childs:
|
||||
#Add target_child to filter
|
||||
new_filters = copy.copy(filters)
|
||||
for i in range(len(filters)):
|
||||
fname, op, val = filters[i]
|
||||
if fname == CLASS_ID_FIELDNAME:
|
||||
logger.warning("Dirty drop of filter : '%s %s %s'" % (
|
||||
fname, op, val))
|
||||
del(new_filters[i])
|
||||
new_filters.append(
|
||||
(CLASS_ID_FIELDNAME, '=',
|
||||
collection_name(target_child.__name__)))
|
||||
result += act(
|
||||
target = target_child,
|
||||
filters = new_filters,
|
||||
relational_filters = relational_filters,
|
||||
**kwargs)
|
||||
return result
|
||||
|
||||
##@brief Connect to database
|
||||
#@note this method avoid opening two times the same connection using
|
||||
#MongoDbDatasource::_connections static attribute
|
||||
#@param username str
|
||||
#@param password str
|
||||
#@param ro bool : If True the Datasource is for read only, else the
|
||||
def __connect(self, username, password, db_name, ro):
|
||||
conn_string = connection_string(
|
||||
username = username, password = password,
|
||||
host = self.__db_infos['host'],
|
||||
port = self.__db_infos['port'],
|
||||
db_name = db_name,
|
||||
ro = ro)
|
||||
|
||||
self.__conn_hash = conn_h = hash(conn_string)
|
||||
if conn_h in self._connections:
|
||||
self._connections[conn_h]['conn_count'] += 1
|
||||
return self._connections[conn_h]['db'][self.__db_infos['db_name']]
|
||||
else:
|
||||
logger.info("Opening a new connection to database")
|
||||
self._connections[conn_h] = {
|
||||
'conn_count': 1,
|
||||
'db': utils.connect(conn_string)}
|
||||
return self._connections[conn_h]['db'][self.__db_infos['db_name']]
|
||||
|
||||
|
||||
##@brief Return a pymongo collection given a LeObject child class
|
||||
#@param leobject LeObject child class (no instance)
|
||||
#return a pymongo.collection instance
|
||||
def __collection(self, leobject):
|
||||
return self.database[object_collection_name(leobject)]
|
||||
|
||||
##@brief Perform subqueries implies by relational filters and append the
|
||||
# result to existing filters
|
||||
#
|
||||
#The processing is divided in multiple steps :
|
||||
# - determine (for each relational field of the target) every collection
|
||||
#that are involved
|
||||
# - generate subqueries for relational_filters that concerns a different
|
||||
#collection than target collection
|
||||
#filters
|
||||
# - execute subqueries
|
||||
# - transform subqueries results in filters
|
||||
# - merge subqueries generated filters with existing filters
|
||||
#
|
||||
#@param target LeObject subclass (no instance) : Target class
|
||||
#@param filters list : List of tuple(FIELDNAME, OP, VALUE)
|
||||
#@param relational_filters : same composition thant filters except that
|
||||
# FIELD is represented by a tuple(FIELDNAME, {CLASS1:RFIELD1,
|
||||
# CLASS2:RFIELD2})
|
||||
#@return a list of pymongo filters ( dict {FIELD:{OPERATOR:VALUE}} )
|
||||
def __process_filters(self,target, filters, relational_filters):
|
||||
# Simple filters lodel2 -> pymongo converting
|
||||
res = self.__filters2mongo(filters, target)
|
||||
rfilters = self.__prepare_relational_filters(target, relational_filters)
|
||||
#Now that everything is well organized, begin to forge subquerie
|
||||
#filters
|
||||
self.__subqueries_from_relational_filters(target, rfilters)
|
||||
# Executing subqueries, creating filters from result, and injecting
|
||||
# them in original filters of the query
|
||||
if len(rfilters) > 0:
|
||||
logger.debug("Begining subquery execution")
|
||||
for fname in rfilters:
|
||||
if fname not in res:
|
||||
res[fname] = dict()
|
||||
subq_results = set()
|
||||
for leobject, sq_filters in rfilters[fname].items():
|
||||
uid_fname = mongo_fieldname(leobject._uid)
|
||||
log_msg = "Subquery running on collection {coll} with filters \
|
||||
'{filters}'"
|
||||
logger.debug(log_msg.format(
|
||||
coll=object_collection_name(leobject),
|
||||
filters=sq_filters))
|
||||
|
||||
cursor = self.__collection(leobject).find(
|
||||
filter=sq_filters,
|
||||
projection=uid_fname)
|
||||
subq_results |= set(doc[uid_fname] for doc in cursor)
|
||||
#generating new filter from result
|
||||
if '$in' in res[fname]:
|
||||
#WARNING we allready have a IN on this field, doing dedup
|
||||
#from result
|
||||
deduped = set(res[fname]['$in']) & subq_results
|
||||
if len(deduped) == 0:
|
||||
del(res[fname]['$in'])
|
||||
else:
|
||||
res[fname]['$in'] = list(deduped)
|
||||
else:
|
||||
res[fname]['$in'] = list(subq_results)
|
||||
if len(rfilters) > 0:
|
||||
logger.debug("End of subquery execution")
|
||||
return res
|
||||
|
||||
##@brief Generate subqueries from rfilters tree
|
||||
#
|
||||
#Returned struct organization :
|
||||
# - 1st level keys : relational field name of target
|
||||
# - 2nd level keys : referenced leobject
|
||||
# - 3th level values : pymongo filters (dict)
|
||||
#
|
||||
#@note The only caller of this method is __process_filters
|
||||
#@warning No return value, the rfilters arguement is modified by
|
||||
#reference
|
||||
#
|
||||
#@param target LeObject subclass (no instance) : Target class
|
||||
#@param rfilters dict : A struct as returned by
|
||||
#MongoDbDatasource.__prepare_relational_filters()
|
||||
#@return None, the rfilters argument is modified by reference
|
||||
@classmethod
|
||||
def __subqueries_from_relational_filters(cls, target, rfilters):
|
||||
for fname in rfilters:
|
||||
for leobject in rfilters[fname]:
|
||||
for rfield in rfilters[fname][leobject]:
|
||||
#This way of doing is not optimized but allows to trigger
|
||||
#warnings in some case (2 different values for a same op
|
||||
#on a same field on a same collection)
|
||||
mongofilters = cls.__op_value_listconv(
|
||||
rfilters[fname][leobject][rfield], target.field(fname))
|
||||
rfilters[fname][leobject][rfield] = mongofilters
|
||||
|
||||
##@brief Generate a tree from relational_filters
|
||||
#
|
||||
#The generated struct is a dict with :
|
||||
# - 1st level keys : relational field name of target
|
||||
# - 2nd level keys : referenced leobject
|
||||
# - 3th level keys : referenced field in referenced class
|
||||
# - 4th level values : list of tuple(op, value)
|
||||
#
|
||||
#@note The only caller of this method is __process_filters
|
||||
#@warning An assertion is done : if two leobject are stored in the same
|
||||
#collection they share the same uid
|
||||
#
|
||||
#@param target LeObject subclass (no instance) : Target class
|
||||
#@param relational_filters : same composition thant filters except that
|
||||
#@return a struct as described above
|
||||
@classmethod
|
||||
def __prepare_relational_filters(cls, target, relational_filters):
|
||||
# We are going to regroup relationnal filters by reference field
|
||||
# then by collection
|
||||
rfilters = dict()
|
||||
if relational_filters is None:
|
||||
relational_filters = []
|
||||
for (fname, rfields), op, value in relational_filters:
|
||||
if fname not in rfilters:
|
||||
rfilters[fname] = dict()
|
||||
rfilters[fname] = dict()
|
||||
# Stores the representative leobject for associated to a collection
|
||||
# name
|
||||
leo_collname = dict()
|
||||
# WARNING ! Here we assert that all leobject that are stored
|
||||
# in a same collection are identified by the same field
|
||||
for leobject, rfield in rfields.items():
|
||||
#here we are filling a dict with leobject as index but
|
||||
#we are doing a UNIQ on collection name
|
||||
cur_collname = object_collection_name(leobject)
|
||||
if cur_collname not in leo_collname:
|
||||
leo_collname[cur_collname] = leobject
|
||||
rfilters[fname][leobject] = dict()
|
||||
#Fecthing the collection's representative leobject
|
||||
repr_leo = leo_collname[cur_collname]
|
||||
|
||||
if rfield not in rfilters[fname][repr_leo]:
|
||||
rfilters[fname][repr_leo][rfield] = list()
|
||||
rfilters[fname][repr_leo][rfield].append((op, value))
|
||||
return rfilters
|
||||
|
||||
##@brief Convert lodel2 filters to pymongo conditions
|
||||
#@param filters list : list of lodel filters
|
||||
#@return dict representing pymongo conditions
|
||||
@classmethod
|
||||
def __filters2mongo(cls, filters, target):
|
||||
res = dict()
|
||||
eq_fieldname = [] #Stores field with equal comparison OP
|
||||
for fieldname, op, value in filters:
|
||||
oop = op
|
||||
ovalue = value
|
||||
op, value = cls.__op_value_conv(op, value, target.field(fieldname))
|
||||
if op == '=':
|
||||
eq_fieldname.append(fieldname)
|
||||
if fieldname in res:
|
||||
logger.warning("Dropping previous condition. Overwritten \
|
||||
by an equality filter")
|
||||
res[fieldname] = value
|
||||
continue
|
||||
if fieldname in eq_fieldname:
|
||||
logger.warning("Dropping condition : '%s %s %s'" % (
|
||||
fieldname, op, value))
|
||||
continue
|
||||
|
||||
if fieldname not in res:
|
||||
res[fieldname] = dict()
|
||||
if op in res[fieldname]:
|
||||
logger.warning("Dropping condition : '%s %s %s'" % (
|
||||
fieldname, op, value))
|
||||
else:
|
||||
if op not in cls.lodel2mongo_op_map:
|
||||
raise ValueError("Invalid operator : '%s'" % op)
|
||||
new_op = cls.lodel2mongo_op_map[op]
|
||||
res[fieldname][new_op] = value
|
||||
return res
|
||||
|
||||
|
||||
##@brief Convert lodel2 operator and value to pymongo struct
|
||||
#
|
||||
#Convertion is done using MongoDbDatasource::lodel2mongo_op_map
|
||||
#@param op str : take value in LeFilteredQuery::_query_operators
|
||||
#@param value mixed : the value
|
||||
#@return a tuple(mongo_op, mongo_value)
|
||||
@classmethod
|
||||
def __op_value_conv(cls, op, value, dhdl):
|
||||
if op not in cls.lodel2mongo_op_map:
|
||||
msg = "Invalid operator '%s' found" % op
|
||||
raise MongoDbDataSourceError(msg)
|
||||
mongop = cls.lodel2mongo_op_map[op]
|
||||
mongoval = value
|
||||
#Converting lodel2 wildcarded string into a case insensitive
|
||||
#mongodb re
|
||||
if mongop in cls.mongo_op_re:
|
||||
if value.startswith('(') and value.endswith(')') and ',' in value:
|
||||
if (dhdl.cast_type is not None):
|
||||
mongoval = [ dhdl.cast_type(item) for item in mongoval[1:-1].split(',') ]
|
||||
else:
|
||||
mongoval = [ item for item in mongoval[1:-1].split(',') ]
|
||||
elif mongop == 'like':
|
||||
#unescaping \
|
||||
mongoval = value.replace('\\\\','\\')
|
||||
if not mongoval.startswith('*'):
|
||||
mongoval = '^'+mongoval
|
||||
#For the end of the string it's harder to detect escaped *
|
||||
if not (mongoval[-1] == '*' and mongoval[-2] != '\\'):
|
||||
mongoval += '$'
|
||||
#Replacing every other unescaped wildcard char
|
||||
mongoval = cls.wildcard_re.sub('.*', mongoval)
|
||||
mongoval = {'$regex': mongoval, '$options': 'i'}
|
||||
return (op, mongoval)
|
||||
|
||||
##@brief Convert a list of tuple(OP, VALUE) into a pymongo filter dict
|
||||
#@return a dict with mongo op as key and value as value...
|
||||
@classmethod
|
||||
def __op_value_listconv(cls, op_value_list, dhdl):
|
||||
result = dict()
|
||||
for op, value in op_value_list:
|
||||
mongop, mongoval = cls.__op_value_conv(op, value, dhdl)
|
||||
if mongop in result:
|
||||
warnings.warn("Duplicated value given for a single \
|
||||
field/operator couple in a query. We will keep only the first one")
|
||||
else:
|
||||
result[mongop] = mongoval
|
||||
return result
|
||||
|
||||
##@brief Generate a comparison function for post reccursion sorting in
|
||||
#select
|
||||
#@return a lambda function that take 2 dict as arguement
|
||||
@classmethod
|
||||
def __generate_lambda_cmp_order(cls, order):
|
||||
if len(order) == 0:
|
||||
return lambda a,b: 0
|
||||
glco = cls.__generate_lambda_cmp_order
|
||||
fname, cmpdir = order[0]
|
||||
order = order[1:]
|
||||
return lambda a,b: glco(order) if a[fname] == b[fname] else (\
|
||||
1 if (a[fname]>b[fname] if cmpdir == 'ASC' else a[fname]<b[fname])\
|
||||
else -1)
|
||||
|
||||
|
||||
##@brief Correct some datas before giving them to pymongo
|
||||
#
|
||||
#For example sets has to be casted to lise
|
||||
#@param datas
|
||||
#@return datas
|
||||
@classmethod
|
||||
def _data_cast(cls, datas):
|
||||
for dname in datas:
|
||||
if isinstance(datas[dname], set):
|
||||
#pymongo raises :
|
||||
#bson.errors.InvalidDocument: Cannot encode object: {...}
|
||||
#with sets
|
||||
datas[dname] = list(datas[dname])
|
||||
return datas
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.exceptions': ['LodelException', 'LodelExceptions',
|
||||
'LodelFatalError', 'DataNoneValid', 'FieldValidationError']})
|
||||
|
||||
#@ingroup plugin_mongodb_datasource
|
||||
class MongoDbDataSourceError(Exception):
|
||||
pass
|
||||
|
||||
##@ingroup plugin_mongodb_datasource
|
||||
class MongoDbConsistencyError(MongoDbDataSourceError):
|
||||
pass
|
||||
|
||||
##@ingroup plugin_mongodb_datasource
|
||||
class MongoDbConsistencyFatalError(LodelFatalError):
|
||||
pass
|
||||
|
||||
##@ingroup plugin_mongodb_datasource
|
||||
class MigrationHandlerChangeError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
##@ingroup plugin_mongodb_datasource
|
||||
class MigrationHandlerError(Exception):
|
||||
pass
|
||||
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.plugin': ['LodelHook']})
|
||||
|
||||
from .datasource import MongoDbDatasource as Datasource
|
||||
|
||||
def migration_handler_class():
|
||||
from .migration_handler import MigrationHandler as migration_handler
|
||||
return migration_handler
|
||||
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.editorial_model.components': ['EmClass', 'EmField'],
|
||||
'lodel.editorial_model.model': ['EditorialModel'],
|
||||
'lodel.leapi.datahandlers.base_classes': ['DataHandler'],
|
||||
'lodel.plugin': ['LodelHook'],
|
||||
'lodel.logger': 'logger'})
|
||||
|
||||
from leapi_dyncode import * #<-- TODO : handle this !!!
|
||||
|
||||
from .utils import connect, object_collection_name, mongo_fieldname
|
||||
from .datasource import MongoDbDatasource
|
||||
from .exceptions import *
|
||||
|
||||
class MigrationHandler(object):
|
||||
|
||||
## @brief Constructs a MongoDbMigrationHandler
|
||||
# @param host str
|
||||
# @param port str
|
||||
# @param db_name str
|
||||
# @param username str
|
||||
# @param password str
|
||||
# @param charset str
|
||||
# @param dry_run bool
|
||||
# @param drop_if_exists bool : drops the table if it already exists
|
||||
def __init__(self, host, port, db_name, username, password,
|
||||
charset='utf-8', dry_run = False, drop_if_exists = False):
|
||||
|
||||
self.database = connect(host, port, db_name, username, password)
|
||||
self.dry_run = dry_run
|
||||
self.drop_if_exists = drop_if_exists
|
||||
self.charset = charset # Useless ?
|
||||
|
||||
logger.debug("MongoDb migration handler instanciated on db : \
|
||||
%s@%s:%s" % (db_name, host, port))
|
||||
|
||||
## @brief Installs the basis collections of the database
|
||||
def init_db(self, emclass_list):
|
||||
for collection_name in [ object_collection_name(cls)
|
||||
for cls in emclass_list]:
|
||||
self._create_collection(collection_name)
|
||||
|
||||
## @brief Creates a collection in the database
|
||||
# @param collection_name str
|
||||
def _create_collection(self, collection_name):
|
||||
existing = self.database.collection_names(
|
||||
include_system_collections=False)
|
||||
if collection_name in existing:
|
||||
if self.drop_if_exists:
|
||||
self._delete_collection(collection_name)
|
||||
logger.debug("Collection %s deleted before creating \
|
||||
it again" % collection_name)
|
||||
self.database.create_collection(name=collection_name)
|
||||
else:
|
||||
logger.info("Collection %s allready exists. \
|
||||
Doing nothing..." % collection_name)
|
||||
else:
|
||||
self.database.create_collection(name=collection_name)
|
||||
logger.debug("Collection %s created" % collection_name)
|
||||
|
||||
## @brief Deletes a collection in the database
|
||||
# @param collection_name str
|
||||
def _delete_collection(self, collection_name):
|
||||
collection = self.database[collection_name]
|
||||
collection.drop_indexes()
|
||||
collection.drop()
|
||||
|
||||
## @brief Performs a change in the Database, corresponding to an Editorial Model change
|
||||
# @param model EditorialModel
|
||||
# @param uid str : the uid of the changing component
|
||||
# @param initial_state dict|None : dictionnary of the initial state of the component, None means it's a creation
|
||||
# @param new_state dict|None: dictionnary of the new state of the component, None means it's a deletion
|
||||
# @note Only the changing properties are added in these state dictionaries
|
||||
# @throw ValueError if no state has been precised or if the component considered in the change is neither an EmClass nor an EmField instance
|
||||
def register_change(self, model, uid, initial_state, new_state):
|
||||
|
||||
if initial_state is None and new_state is None:
|
||||
raise ValueError('An Editorial Model change should have at least one state precised (initial or new), '
|
||||
'none given here')
|
||||
|
||||
if initial_state is None:
|
||||
state_change = 'new'
|
||||
elif new_state is None:
|
||||
state_change = 'del'
|
||||
else:
|
||||
state_change = 'upgrade'
|
||||
|
||||
component_class_name = None
|
||||
if isinstance(model.classes(uid), EmClass):
|
||||
component_class_name = 'emclass'
|
||||
elif isinstance(model.classes(uid), EmField):
|
||||
component_class_name = 'emfield'
|
||||
|
||||
if component_class_name:
|
||||
handler_func = '_'+component_class_name.lower()+'_'+state_change
|
||||
if hasattr(self, handler_func):
|
||||
getattr(self, handler_func)(model, uid, initial_state, new_state)
|
||||
else:
|
||||
raise ValueError("The component concerned should be an EmClass or EmField instance, %s given",
|
||||
model.classes(uid).__class__)
|
||||
|
||||
def register_model_state(self, em, state_hash):
|
||||
pass
|
||||
|
||||
## @brief creates a new collection corresponding to a given uid
|
||||
# @see register_change()
|
||||
def _emclass_new(self, model, uid, initial_state, new_state):
|
||||
collection_name = object_collection_name(model.classes(uid))
|
||||
self._create_collection(collection_name)
|
||||
|
||||
## @brief deletes a collection corresponding to a given uid
|
||||
# @see register_change()
|
||||
def _emclass_delete(self, model, uid, initial_state, new_state):
|
||||
collection_name = object_collection_name(model.classes(uid))
|
||||
self._delete_collection(collection_name)
|
||||
|
||||
## @brief creates a new field in a collection
|
||||
# @see register_change()
|
||||
def _emfield_new(self, model, uid, initial_state, new_state):
|
||||
if new_state['data_handler'] == 'relation':
|
||||
class_name = self.class_collection_name_from_field(model, new_state)
|
||||
self._create_field_in_collection(class_name, uid, new_state)
|
||||
else:
|
||||
collection_name = self._class_collection_name_from_field(model, new_state)
|
||||
field_definition = self._field_definition(new_state['data_handler'], new_state)
|
||||
self._create_field_in_collection(collection_name, uid, field_definition)
|
||||
|
||||
## @brief deletes a field in a collection
|
||||
# @see register_change()
|
||||
def _emfield_del(self, model, uid, initial_state, new_state):
|
||||
collection_name = self._class_collection_name_from_field(model, initial_state)
|
||||
field_name = mongo_fieldname(model.field(uid).name)
|
||||
self._delete_field_in_collection(collection_name, field_name)
|
||||
|
||||
## @brief upgrades a field
|
||||
def _emfield_upgrade(self, model, uid, initial_state, new_state):
|
||||
collection_name = self._class_collection_name_from_field(model, initial_state)
|
||||
field_name = mongo_fieldname(model.field(uid).name)
|
||||
self._check_field_in_collection(collection_name, field_name, initial_state, new_state)
|
||||
|
||||
def _check_field_in_collection(self,collection_name, field_name, initial_sate, new_state):
|
||||
collection = self.database[collection_name]
|
||||
field_name = mongo_fieldname(field_name)
|
||||
cursor = collection.find({field_name: {'$exists': True}}, {field_name: 1})
|
||||
for document in cursor:
|
||||
# TODO vérifier que le champ contient une donnée compatible (document[field_name])
|
||||
pass
|
||||
|
||||
## @brief Defines the default value when a new field is added to a collection's items
|
||||
# @param fieldtype str : name of the field's type
|
||||
# @param options dict : dictionary giving the options to use to initiate the field's value.
|
||||
# @return dict (containing a 'default' key with the default value)
|
||||
def _field_definition(self, fieldtype, options):
|
||||
basic_type = DataHandler.from_name(fieldtype).ftype
|
||||
if basic_type == 'datetime':
|
||||
if 'now_on_create' in options and options['now_on_create']:
|
||||
return {'default': datetime.datetime.utcnow()}
|
||||
if basic_type == 'relation':
|
||||
return {'default': []}
|
||||
|
||||
return {'default': ''}
|
||||
|
||||
def _class_collection_name_from_field(self, model, field):
|
||||
class_id = field['class_id']
|
||||
component_class = model.classes(class_id)
|
||||
component_collection = object_collection_name(component_class)
|
||||
return component_collection
|
||||
|
||||
|
||||
## @brief Creates a new field in a collection
|
||||
# @param collection_name str
|
||||
# @param field str
|
||||
# @param options dict
|
||||
def _create_field_in_collection(self, collection_name, field, options):
|
||||
emfield = EmField(field)
|
||||
field_name = mongo_fieldname(field)
|
||||
self.database[collection_name].update_many({'uid': emfield.get_emclass_uid(), field_name: {'$exists': False}},
|
||||
{'$set': {field_name: options['default']}}, False)
|
||||
|
||||
## @brief Deletes a field in a collection
|
||||
# @param collection_name str
|
||||
# @param field_name str
|
||||
def _delete_field_in_collection(self, collection_name, field_name):
|
||||
if field_name != '_id':
|
||||
field_name = mongo_fieldname(field_name)
|
||||
self.database[collection_name].update_many({field_name: {'$exists': True}},
|
||||
{'$unset': {field_name:1}}, False)
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import pymongo
|
||||
from pymongo import MongoClient
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.settings.settings': [('Settings', 'settings')],
|
||||
'lodel.logger': 'logger'})
|
||||
|
||||
common_collections = {
|
||||
'object': 'objects',
|
||||
'relation': 'relation'
|
||||
}
|
||||
|
||||
LODEL_SORT_OPERATORS_MAP = {
|
||||
'ASC': pymongo.ASCENDING,
|
||||
'DESC': pymongo.DESCENDING
|
||||
}
|
||||
|
||||
MONGODB_SORT_OPERATORS_MAP = {
|
||||
'ASC': 1,
|
||||
'DESC': -1
|
||||
}
|
||||
|
||||
MANDATORY_CONNECTION_ARGS = ('host', 'port', 'login', 'password', 'dbname')
|
||||
|
||||
|
||||
class MongoDbConnectionError(Exception):
|
||||
pass
|
||||
|
||||
##@brief Forge a mongodb uri connection string
|
||||
#@param host str : hostname
|
||||
#@param port int|str : port number
|
||||
#@param username str
|
||||
#@param password str
|
||||
#@param db_name str : the db to authenticate on (mongo as auth per db)
|
||||
#@param ro bool : if True open a read_only connection
|
||||
#@return a connection string
|
||||
#@see https://docs.mongodb.com/v2.4/reference/connection-string/#connection-string-options
|
||||
#@todo escape arguments
|
||||
def connection_string(host, port, username, password, db_name = None, ro = None):
|
||||
ret = 'mongodb://'
|
||||
if username != None:
|
||||
ret += username
|
||||
if password != None:
|
||||
ret += ':'+password
|
||||
ret+='@'
|
||||
elif password != None:
|
||||
raise RuntimeError("Password given but no username given...")
|
||||
host = 'localhost' if host is None else host
|
||||
ret += host
|
||||
if port is not None:
|
||||
ret += ':'+str(port)
|
||||
if db_name is not None:
|
||||
ret += '/'+db_name
|
||||
else:
|
||||
logger.warning("No database indicated. Huge chance for authentication \
|
||||
to fails")
|
||||
if ro:
|
||||
ret += '?readOnly='+str(bool(ro))
|
||||
return ret
|
||||
|
||||
##@brief Return an instanciated MongoClient from a connstring
|
||||
#@param connstring str : as returned by connection_string() method
|
||||
#@return A MongoClient instance
|
||||
def connect(connstring):
|
||||
return MongoClient(connstring)
|
||||
|
||||
## @brief Returns a collection name given a EmClass
|
||||
# @param class_object EmClass
|
||||
# @return str
|
||||
def object_collection_name(class_object):
|
||||
return class_object.__name__
|
||||
|
||||
def collection_name(class_name):
|
||||
return class_name
|
||||
|
||||
## @brief Determine a collection field name given a lodel2 fieldname
|
||||
# @note For the moment this method only return the argument but EVERYWHERE
|
||||
# in the datasource we should use this method to gather proper fieldnames
|
||||
# @param fieldname str : A lodel2 fieldname
|
||||
# @return A string representing a well formated mongodb fieldname
|
||||
# @see mongo_filednames
|
||||
def mongo_fieldname(fieldname):
|
||||
return fieldname
|
||||
|
||||
|
||||
## @brief Same as mongo_fieldname but for list of fields
|
||||
#
|
||||
# A small utility function
|
||||
# @param fieldnames iterable : contains str only
|
||||
# @return a list of converted fildnames (str)
|
||||
# @see mongo_fieldname
|
||||
def mongo_fieldnames(fieldnames):
|
||||
return [mongo_fieldname(fname) for fname in fieldnames]
|
||||
|
||||
|
||||
## @brief Returns a list of orting options
|
||||
# @param query_filters_order list
|
||||
# @return list
|
||||
def parse_query_order(query_filters_order):
|
||||
return [(field, LODEL_SORT_OPERATORS_MAP[direction])
|
||||
for field, direction in query_filters_order]
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.settings.validator': ['SettingValidator']})
|
||||
|
||||
__plugin_name__ = "multisite"
|
||||
__version__ = '0.0.1' #or __version__ = [0,0,1]
|
||||
__loader__ = "main.py"
|
||||
__author__ = "Lodel2 dev team"
|
||||
__fullname__ = "Multisite plugin"
|
||||
__name__ = 'yweber.dummy'
|
||||
__plugin_type__ = 'extension'
|
||||
|
||||
CONFSPEC = {
|
||||
'lodel2.server': {
|
||||
'port': (80,SettingValidator('int')),
|
||||
'listen_addr': ('', SettingValidator('string')),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
#Known bugs :
|
||||
#Sockets are not closed properly leading in a listening socket leak
|
||||
#
|
||||
|
||||
|
||||
import wsgiref
|
||||
import wsgiref.simple_server
|
||||
from wsgiref.simple_server import make_server
|
||||
import http.server
|
||||
import multiprocessing as mp
|
||||
import socketserver
|
||||
import socket
|
||||
import os
|
||||
from io import BufferedWriter
|
||||
import urllib
|
||||
import threading
|
||||
|
||||
import sys, signal
|
||||
|
||||
from lodel.context import LodelContext
|
||||
from lodel.context import ContextError
|
||||
|
||||
LISTEN_ADDR = ''
|
||||
LISTEN_PORT = 1337
|
||||
|
||||
SHUTDOWN_POLL_INTERVAL = 0.1
|
||||
|
||||
class HtppHandler(wsgiref.simple_server.WSGIRequestHandler):
|
||||
def handle(self):
|
||||
print("addr : %s %s\n" % (self.client_address, type(self.request)))
|
||||
#Dirty copy & past from Lib/http/server.py in Cpython sources
|
||||
try:
|
||||
self.raw_requestline = self.rfile.readline(65537)
|
||||
if len(self.raw_requestline) > 65536:
|
||||
self.requestline = ''
|
||||
self.request_version = ''
|
||||
self.command = ''
|
||||
self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG)
|
||||
return
|
||||
if not self.raw_requestline:
|
||||
self.close_connection = True
|
||||
return
|
||||
if not self.parse_request():
|
||||
return
|
||||
#Here begin custom code
|
||||
env = self.get_environ()
|
||||
stdout = BufferedWriter(self.wfile)
|
||||
try:
|
||||
handler = wsgiref.handlers.SimpleHandler(
|
||||
self.rfile, stdout, self.get_stderr(), env)
|
||||
handler.request_handler = self # backpointer for logging
|
||||
handler.run(self.server.get_app())
|
||||
finally:
|
||||
stdout.detach()
|
||||
except socket.timeout as e:
|
||||
self.log_error("Request timed out: %r", e)
|
||||
self.close_connection = True
|
||||
return
|
||||
|
||||
##@brief An attempt to solve the socket leak problem
|
||||
def close(self):
|
||||
print("Closing request from handler : %s" % self.request)
|
||||
self.request.close()
|
||||
super().close()
|
||||
|
||||
##@brief Copy of wsgiref.simple_server.WSGIRequestHandler.get_environ method
|
||||
def get_environ(self):
|
||||
env = self.server.base_environ.copy()
|
||||
env['SERVER_PROTOCOL'] = self.request_version
|
||||
env['SERVER_SOFTWARE'] = self.server_version
|
||||
env['REQUEST_METHOD'] = self.command
|
||||
if '?' in self.path:
|
||||
path,query = self.path.split('?',1)
|
||||
else:
|
||||
path,query = self.path,''
|
||||
|
||||
env['PATH_INFO'] = urllib.parse.unquote(path, 'iso-8859-1')
|
||||
env['QUERY_STRING'] = query
|
||||
|
||||
host = self.address_string()
|
||||
if host != self.client_address[0]:
|
||||
env['REMOTE_HOST'] = host
|
||||
env['REMOTE_ADDR'] = self.client_address[0]
|
||||
|
||||
if self.headers.get('content-type') is None:
|
||||
env['CONTENT_TYPE'] = self.headers.get_content_type()
|
||||
else:
|
||||
env['CONTENT_TYPE'] = self.headers['content-type']
|
||||
|
||||
length = self.headers.get('content-length')
|
||||
if length:
|
||||
env['CONTENT_LENGTH'] = length
|
||||
|
||||
for k, v in self.headers.items():
|
||||
k=k.replace('-','_').upper(); v=v.strip()
|
||||
if k in env:
|
||||
continue # skip content length, type,etc.
|
||||
if 'HTTP_'+k in env:
|
||||
env['HTTP_'+k] += ','+v # comma-separate multiple headers
|
||||
else:
|
||||
env['HTTP_'+k] = v
|
||||
return env
|
||||
|
||||
##@brief Speciallized ForkingTCPServer to fit specs of WSGIHandler
|
||||
class HttpServer(socketserver.ForkingTCPServer):
|
||||
|
||||
##@brief Onverwritting of ForkingTCPServer.server_bind method
|
||||
#to fit the wsgiref specs
|
||||
def server_bind(self):
|
||||
super().server_bind()
|
||||
#Copy & paste from Lib/http/server.py
|
||||
host, port = self.socket.getsockname()[:2]
|
||||
self.server_name = socket.getfqdn(host)
|
||||
self.server_port = port
|
||||
# Copy&paste from Lib/wsgiref/simple_server.py
|
||||
# Set up base environment
|
||||
env = self.base_environ = {}
|
||||
env['SERVER_NAME'] = self.server_name
|
||||
env['GATEWAY_INTERFACE'] = 'CGI/1.1'
|
||||
env['SERVER_PORT'] = str(self.server_port)
|
||||
env['REMOTE_HOST']=''
|
||||
env['CONTENT_LENGTH']=''
|
||||
env['SCRIPT_NAME'] = ''
|
||||
|
||||
##@brief Hardcoded callback function
|
||||
def get_app(self):
|
||||
return wsgi_router
|
||||
|
||||
##@brief An attempt to solve the socket leak problem
|
||||
def close_request(self, request):
|
||||
print("Closing client socket in server : %s" % request)
|
||||
request.close()
|
||||
|
||||
##@brief An attempt to solve the socket leak problem
|
||||
def server_close(self):
|
||||
print("Closing listening socket")
|
||||
self.socket.close()
|
||||
|
||||
##@brief utility function to extract site id from an url
|
||||
def site_id_from_url(url):
|
||||
res = ''
|
||||
for c in url[1:]:
|
||||
if c == '/':
|
||||
break
|
||||
res += c
|
||||
if len(res) == 0:
|
||||
return None
|
||||
return res
|
||||
|
||||
##@brief Utility function to return quickly an error
|
||||
def http_error(env, start_response, status = '500 internal server error', \
|
||||
extra = None):
|
||||
headers = [('Content-type', 'text/plain; charset=utf-8')]
|
||||
start_response(status, headers)
|
||||
msg = status
|
||||
if extra is not None:
|
||||
msg = extra
|
||||
return [msg.encode('utf-8')]
|
||||
|
||||
##@brief This method is run in a child process by the handler
|
||||
def wsgi_router(env, start_response):
|
||||
print("\n\nCPROCPID = %d\n\n" % os.getpid()) #<-- print PID (for debug)
|
||||
#Attempt to load a context
|
||||
site_id = site_id_from_url(env['PATH_INFO'])
|
||||
if site_id is None:
|
||||
#It can be nice to provide a list of instances here
|
||||
return http_error(env, start_response, '404 Not Found')
|
||||
try:
|
||||
LodelContext.set(site_id)
|
||||
#We are in the good context
|
||||
|
||||
except ContextError as e:
|
||||
print(e)
|
||||
return http_error(env, start_response, '404 Not found',
|
||||
"No site named '%s'" % site_id)
|
||||
#
|
||||
# Here we have to put the code that run the request
|
||||
#
|
||||
|
||||
#Testing purpose
|
||||
rep = "Woot '%s'" % site_id
|
||||
print(rep)
|
||||
start_response('200 ok', [('Content-type', 'text/plain; charset=utf-8')])
|
||||
return [rep.encode('utf-8')]
|
||||
|
||||
#mp.Process(target=foo, args=(env,start_response))
|
||||
return child_proc(env, start_response)
|
||||
|
||||
def main_loop():
|
||||
|
||||
#Set the start method for multiprocessing
|
||||
mp.set_start_method('forkserver')
|
||||
print("\n\nPID = %d\n\n" % os.getpid())
|
||||
|
||||
listen_addr = LISTEN_ADDR
|
||||
listen_port = LISTEN_PORT
|
||||
|
||||
#server = socketserver.ForkingTCPServer((listen_addr, listen_port),
|
||||
# HtppHandler)
|
||||
server = HttpServer((listen_addr, listen_port),
|
||||
HtppHandler)
|
||||
|
||||
#Signal handler to close server properly on sigint
|
||||
def sigint_handler(signal, frame):
|
||||
print("Ctrl-c pressed, exiting")
|
||||
server.shutdown() # <-- Do not work for unkonwn reasons
|
||||
server.server_close()
|
||||
sys.exit(0)
|
||||
#signal.signal(signal.SIGINT, sigint_handler)
|
||||
|
||||
server.serve_forever(SHUTDOWN_POLL_INTERVAL)
|
||||
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.settings.validator': ['SettingValidator']})
|
||||
|
||||
__plugin_name__ = 'ram_session'
|
||||
__version__ = [0,0,1]
|
||||
__plugin_type__ = 'session_handler'
|
||||
__loader__ = 'main.py'
|
||||
__author__ = "Lodel2 dev team"
|
||||
__fullname__ = "RAM Session Store Plugin"
|
||||
|
||||
CONFSPEC = {
|
||||
'lodel2.sessions':{
|
||||
'expiration': (900, SettingValidator('int')),
|
||||
'tokensize': (512, SettingValidator('int')),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
import os
|
||||
import copy
|
||||
import binascii
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.logger': 'logger',
|
||||
'lodel.settings': ['Settings'],
|
||||
'lodel.auth.exceptions': ['ClientError', 'ClientAuthenticationFailure',
|
||||
'ClientPermissionDenied', 'ClientAuthenticationError']})
|
||||
|
||||
__sessions = dict()
|
||||
|
||||
def __generate_token():
|
||||
return binascii.hexlify(os.urandom(Settings.sessions.tokensize//2))
|
||||
|
||||
def _check_token(token):
|
||||
if len(token) != Settings.sessions.tokensize:
|
||||
raise ClientAuthenticationFailure("Malformed session token")
|
||||
if token not in __sessions:
|
||||
raise ClientAuthenticationFailure("No session with this token")
|
||||
|
||||
def start_session():
|
||||
token = __generate_token()
|
||||
__sessions[token] = dict()
|
||||
_check_token(token)
|
||||
logger.debug("New session created")
|
||||
return token
|
||||
|
||||
def destroy_session(token):
|
||||
_check_token(token)
|
||||
del(__sessions[token])
|
||||
logger.debug("Session %s destroyed" % token)
|
||||
|
||||
def restore_session(token):
|
||||
_check_token(token)
|
||||
logger.debug("Restoring session : %s" %__sessions[token])
|
||||
return __sessions[token]
|
||||
|
||||
def save_session(token, datas):
|
||||
_check_token(token)
|
||||
__sessions[token] = copy.copy(datas)
|
||||
logger.debug("Session saved")
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
__plugin_name__ = 'webui'
|
||||
__version__ = '0.0.1'
|
||||
__plugin_type__ = 'ui'
|
||||
__loader__ = 'main.py'
|
||||
__confspec__ = 'confspec.py'
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {'lodel.auth.client': ['Client']})
|
||||
|
||||
class WebUiClient(Client):
|
||||
|
||||
def __init__(self, ip, user_agent, session_token = None):
|
||||
self.__ip = ip
|
||||
self.__user_agent = user_agent
|
||||
super().__init__(session_token = session_token)
|
||||
|
||||
def __str__(self):
|
||||
return "%s (%s)" % (self.__ip, self.__user_agent)
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.settings.validator': ['SettingValidator']})
|
||||
|
||||
CONFSPEC = {
|
||||
'lodel2.webui': {
|
||||
'standalone': ( 'False',
|
||||
SettingValidator('string')),
|
||||
'listen_address': ( '127.0.0.1',
|
||||
SettingValidator('dummy')),
|
||||
'listen_port': ( '9090',
|
||||
SettingValidator('int')),
|
||||
'static_url': ( 'http://127.0.0.1/static/',
|
||||
SettingValidator('regex', pattern = r'^https?://[^/].*$')),
|
||||
'virtualenv': (None,
|
||||
SettingValidator('path', none_is_valid=True)),
|
||||
'uwsgicmd': ('/usr/bin/uwsgi', SettingValidator('dummy')),
|
||||
'cookie_secret_key': ('ConfigureYourOwnCookieSecretKey', SettingValidator('dummy')),
|
||||
'cookie_session_id': ('lodel', SettingValidator('dummy')),
|
||||
'uwsgi_workers': (2, SettingValidator('int'))
|
||||
},
|
||||
'lodel2.webui.sessions': {
|
||||
'directory': ( '/tmp',
|
||||
SettingValidator('path')),
|
||||
'expiration': ( 900,
|
||||
SettingValidator('int')),
|
||||
'file_template': ( 'lodel2_%s.sess',
|
||||
SettingValidator('dummy')),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
class HttpException(Exception):
|
||||
|
||||
STATUS_STR = {
|
||||
4:{
|
||||
400: 'Bad request',
|
||||
401: 'Unauthorized',
|
||||
402: 'Payment required',
|
||||
403: 'Forbidden',
|
||||
404: 'Not found',
|
||||
418: 'I\'m a teapot', #RFC 2324
|
||||
},
|
||||
5:{
|
||||
500: 'Internal server error',
|
||||
501: 'Not implemented',
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, status_code = 500, tpl = 'error.html', custom = None):
|
||||
self.status_code = status_code
|
||||
self.tpl = tpl
|
||||
self.custom = custom
|
||||
|
||||
def render(self, request):
|
||||
from .interface.template.loader import TemplateLoader
|
||||
loader = TemplateLoader()
|
||||
tpl_vars = {
|
||||
'status_code': self.status_code,
|
||||
'status_str': self.status_str(self.status_code),
|
||||
'custom': self.custom }
|
||||
response = Response(
|
||||
loader.render_to_response(self.tpl, template_vars = tpl_vars),
|
||||
mimetype = 'text/html')
|
||||
response.status_code = self.status_code
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def status_str(status_code):
|
||||
status_fam = status_code / 100
|
||||
if status_fam not in HttpException.STATUS_STR or \
|
||||
status_code not in HttpException.STATUS_STR[status_fam]:
|
||||
return 'Unknown'
|
||||
else:
|
||||
return HttpException.STATUS_STR[status_fam][status_code]
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
from .base import *
|
||||
from .admin import *
|
||||
from .document import *
|
||||
from .listing import *
|
||||
from .users import *
|
||||
|
||||
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from ...exceptions import *
|
||||
from .base import get_response
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.leapi.exceptions': [],
|
||||
'lodel.logger': 'logger',
|
||||
'lodel.leapi.datahandlers.base_classes': ['MultipleRef']})
|
||||
|
||||
from ...client import WebUiClient
|
||||
import leapi_dyncode as dyncode
|
||||
import warnings
|
||||
|
||||
LIST_SEPARATOR = ','
|
||||
|
||||
##@brief These functions are called by the rules defined in ../urls.py
|
||||
## To administrate the instance of the editorial model
|
||||
|
||||
##@brief Controller's function to redirect on the home page of the admin
|
||||
# @param request : the request (get or post)
|
||||
# @note the response is given in a html page called in get_response_function
|
||||
def index_admin(request):
|
||||
# We have to be identified to admin the instance
|
||||
# temporary, the acl will be more restrictive
|
||||
#if WebUiClient.is_anonymous():
|
||||
# return get_response('users/signin.html')
|
||||
return get_response('admin/admin.html')
|
||||
|
||||
##@brief Controller's function to update an object of the editorial model
|
||||
# @param request : the request (get or post)
|
||||
# @note the response is given in a html page (in templates/admin) called in get_response_function
|
||||
def admin_update(request):
|
||||
# We have to be identified to admin the instance
|
||||
# temporary, the acl will be more restrictive
|
||||
#if WebUiClient.is_anonymous():
|
||||
# return get_response('users/signin.html')
|
||||
msg=''
|
||||
|
||||
# If the form has been submitted
|
||||
if request.method == 'POST':
|
||||
error = None
|
||||
datas = list()
|
||||
classname = request.form['classname']
|
||||
logger.warning('Composed uids broken here')
|
||||
uid = request.form['uid']
|
||||
try:
|
||||
target_leo = dyncode.Object.name2class(classname)
|
||||
except LeApiError:
|
||||
classname = None
|
||||
if classname is None or target_leo.is_abstract():
|
||||
raise HttpException(400, custom = "Bad classname given")
|
||||
|
||||
leo_to_update = target_leo.get_from_uid(uid)
|
||||
|
||||
errors = dict()
|
||||
for fieldname, value in request.form.items():
|
||||
#We want to drop 2 input named 'classname' and 'uid'
|
||||
if len(fieldname) > 12:
|
||||
#Other input names are : field_input_FIELDNAME
|
||||
#Extract the fieldname
|
||||
fieldname = fieldname[12:]
|
||||
try:
|
||||
dh = leo_to_update.data_handler(fieldname)
|
||||
except NameError as e:
|
||||
errors[fieldname] = e
|
||||
continue
|
||||
#Multiple ref list preparation
|
||||
if issubclass(dh.__class__, MultipleRef):
|
||||
value=[spl for spl in [
|
||||
v.strip() for v in value.split(LIST_SEPARATOR)]
|
||||
if len(spl) > 0]
|
||||
elif len(value.strip()) == 0:
|
||||
value = None
|
||||
try:
|
||||
leo_to_update.set_data(fieldname, value)
|
||||
except Exception as e:
|
||||
errors[fieldname] = e
|
||||
continue
|
||||
if len(errors) > 0:
|
||||
custom_msg = '<h1>Errors in datas</h1><ul>'
|
||||
for fname, error in errors.items():
|
||||
custom_msg += '<li>%s : %s</li>' % (
|
||||
fname, error)
|
||||
custom_msg += '</ul>'
|
||||
raise HttpException(400, custom = custom_msg)
|
||||
leo_to_update.update()
|
||||
|
||||
# Display of the form with the object's values to be updated
|
||||
if 'classname' in request.GET:
|
||||
# We need the class of the object to update
|
||||
classname = request.GET['classname']
|
||||
if len(classname) > 1:
|
||||
raise HttpException(400)
|
||||
classname = classname[0]
|
||||
try:
|
||||
target_leo = dyncode.Object.name2class(classname)
|
||||
except LeApiError:
|
||||
# classname = None
|
||||
raise HttpException(400)
|
||||
logger.warning('Composed uids broken here')
|
||||
uid_field = target_leo.uid_fieldname()[0]
|
||||
|
||||
# We need the uid of the object
|
||||
test_valid = 'lodel_id' in request.GET \
|
||||
and len(request.GET['lodel_id']) == 1
|
||||
|
||||
if test_valid:
|
||||
try:
|
||||
dh = target_leo.field(uid_field)
|
||||
# we cast the uid extrated form the request to the adequate type
|
||||
# given by the datahandler of the uidfield's datahandler
|
||||
lodel_id = dh.cast_type(request.GET['lodel_id'][0])
|
||||
except (ValueError, TypeError):
|
||||
test_valid = False
|
||||
|
||||
if not test_valid:
|
||||
raise HttpException(400)
|
||||
else:
|
||||
# Check if the object actually exists
|
||||
# We get it from the database
|
||||
query_filters = list()
|
||||
query_filters.append((uid_field,'=',lodel_id))
|
||||
obj = target_leo.get(query_filters)
|
||||
if len(obj) == 0:
|
||||
raise HttpException(404)
|
||||
return get_response('admin/admin_edit.html', target=target_leo, lodel_id =lodel_id)
|
||||
|
||||
##@brief Controller's function to create an object of the editorial model
|
||||
# @param request : the request (get or post)
|
||||
# @note the response is given in a html page (in templates/admin) called in get_response_function
|
||||
def admin_create(request):
|
||||
# We have to be identified to admin the instance
|
||||
# temporary, the acl will be more restrictive
|
||||
#if WebUiClient.is_anonymous():
|
||||
# return get_response('users/signin.html')
|
||||
classname = None
|
||||
# If the form has been submitted
|
||||
if request.method == 'POST':
|
||||
error = None
|
||||
datas = list()
|
||||
classname = request.form['classname']
|
||||
try:
|
||||
target_leo = dyncode.Object.name2class(classname)
|
||||
except LeApiError:
|
||||
classname = None
|
||||
if classname is None or target_leo.is_abstract():
|
||||
raise HttpException(400)
|
||||
fieldnames = target_leo.fieldnames()
|
||||
fields = dict()
|
||||
|
||||
for in_put, in_value in request.form.items():
|
||||
# The classname is handled by the datasource, we are not allowed to modify it
|
||||
# both are hidden in the form, to identify the object here
|
||||
if in_put != 'classname' and in_value != '':
|
||||
dhl = target_leo.data_handler(in_put[12:])
|
||||
if dhl.is_reference() and in_value != '' and not dhl.is_singlereference():
|
||||
logger.info(in_value)
|
||||
in_value.replace(" ","")
|
||||
in_value=in_value.split(',')
|
||||
in_value=list(in_value)
|
||||
fields[in_put[12:]] = in_value
|
||||
if in_value == '':
|
||||
fields[in_put[12:]] = None
|
||||
|
||||
# Insertion in the database of the values corresponding to a new object
|
||||
new_uid = target_leo.insert(fields)
|
||||
|
||||
# reurn to the form with a confirmation or error message
|
||||
if not new_uid is None:
|
||||
msg = 'Successfull creation';
|
||||
else:
|
||||
msg = 'Oops something wrong happened...object not saved'
|
||||
return get_response('admin/admin_create.html', target=target_leo, msg = msg)
|
||||
|
||||
# Display of an empty form
|
||||
if 'classname' in request.GET:
|
||||
# We need the class to create an object in
|
||||
classname = request.GET['classname']
|
||||
if len(classname) > 1:
|
||||
raise HttpException(400)
|
||||
classname = classname[0]
|
||||
try:
|
||||
target_leo = dyncode.Object.name2class(classname)
|
||||
except LeApiError:
|
||||
classname = None
|
||||
|
||||
if classname is None or target_leo.is_abstract():
|
||||
raise HttpException(400)
|
||||
return get_response('admin/admin_create.html', target=target_leo)
|
||||
|
||||
##@brief Controller's function to delete an object of the editorial model
|
||||
# @param request : the request (get)
|
||||
# @note the response is given in a html page (in templates/admin) called in get_response_function
|
||||
def admin_delete(request):
|
||||
# We have to be identified to admin the instance
|
||||
# temporary, the acl will be more restrictive
|
||||
#if WebUiClient.is_anonymous():
|
||||
# return get_response('users/signin.html')
|
||||
classname = None
|
||||
|
||||
if 'classname' in request.GET:
|
||||
# We need the class to delete an object in
|
||||
classname = request.GET['classname']
|
||||
if len(classname) > 1:
|
||||
raise HttpException(400)
|
||||
classname = classname[0]
|
||||
try:
|
||||
target_leo = dyncode.Object.name2class(classname)
|
||||
except LeApiError:
|
||||
# classname = None
|
||||
raise HttpException(400)
|
||||
logger.warning('Composed uids broken here')
|
||||
uid_field = target_leo.uid_fieldname()[0]
|
||||
|
||||
# We also need the uid of the object to delete
|
||||
test_valid = 'lodel_id' in request.GET \
|
||||
and len(request.GET['lodel_id']) == 1
|
||||
|
||||
if test_valid:
|
||||
try:
|
||||
dh = target_leo.field(uid_field)
|
||||
# we cast the uid extrated form the request to the adequate type
|
||||
# given by the datahandler of the uidfield's datahandler
|
||||
lodel_id = dh.cast_type(request.GET['lodel_id'][0])
|
||||
except (ValueError, TypeError):
|
||||
test_valid = False
|
||||
|
||||
if not test_valid:
|
||||
raise HttpException(400)
|
||||
else:
|
||||
query_filters = list()
|
||||
query_filters.append((uid_field,'=',lodel_id))
|
||||
nb_deleted = target_leo.delete_bundle(query_filters)
|
||||
|
||||
if nb_deleted == 1:
|
||||
msg = 'Object successfully deleted';
|
||||
else:
|
||||
msg = 'Oops something wrong happened...object still here'
|
||||
|
||||
return get_response('admin/admin_delete.html', target=target_leo, lodel_id =lodel_id, msg = msg)
|
||||
|
||||
|
||||
|
||||
def admin_classes(request):
|
||||
# We have to be identified to admin the instance
|
||||
# temporary, the acl will be more restrictive
|
||||
#if WebUiClient.is_anonymous():
|
||||
# return get_response('users/signin.html')
|
||||
return get_response('admin/list_classes_admin.html', my_classes = dyncode.dynclasses)
|
||||
|
||||
def create_object(request):
|
||||
# We have to be identified to admin the instance
|
||||
# temporary, the acl will be more restrictive
|
||||
#if WebUiClient.is_anonymous():
|
||||
# return get_response('users/signin.html')
|
||||
return get_response('admin/list_classes_create.html', my_classes = dyncode.dynclasses)
|
||||
|
||||
def delete_object(request):
|
||||
# We have to be identified to admin the instance
|
||||
# temporary, the acl will be more restrictive
|
||||
#if WebUiClient.is_anonymous():
|
||||
# return get_response('users/signin.html')
|
||||
return get_response('admin/list_classes_delete.html', my_classes = dyncode.dynclasses)
|
||||
|
||||
def admin_class(request):
|
||||
# We have to be identified to admin the instance
|
||||
# temporary, the acl will be more restrictive
|
||||
#if WebUiClient.is_anonymous():
|
||||
# return get_response('users/signin.html')
|
||||
# We need the class we'll list to select the object to edit
|
||||
if 'classname' in request.GET:
|
||||
classname = request.GET['classname']
|
||||
if len(classname) > 1:
|
||||
raise HttpException(400)
|
||||
classname = classname[0]
|
||||
try:
|
||||
target_leo = dyncode.Object.name2class(classname)
|
||||
except LeApiError:
|
||||
classname = None
|
||||
if classname is None or target_leo.is_abstract():
|
||||
raise HttpException(400)
|
||||
return get_response('admin/show_class_admin.html', target=target_leo)
|
||||
|
||||
def delete_in_class(request):
|
||||
# We have to be identified to admin the instance
|
||||
# temporary, the acl will be more restrictive
|
||||
#if WebUiClient.is_anonymous():
|
||||
# return get_response('users/signin.html')
|
||||
# We need the class we'll list to select the object to delete
|
||||
if 'classname' in request.GET:
|
||||
classname = request.GET['classname']
|
||||
if len(classname) > 1:
|
||||
raise HttpException(400)
|
||||
classname = classname[0]
|
||||
try:
|
||||
target_leo = dyncode.Object.name2class(classname)
|
||||
except LeApiError:
|
||||
classname = None
|
||||
if classname is None or target_leo.is_abstract():
|
||||
raise HttpException(400)
|
||||
return get_response('admin/show_class_delete.html', target=target_leo)
|
||||
|
||||
def admin(request):
|
||||
# We have to be identified to admin the instance
|
||||
# temporary, the acl will be more restrictive
|
||||
#if WebUiClient.is_anonymous():
|
||||
# return get_response('users/signin.html')
|
||||
return get_response('admin/admin.html')
|
||||
|
||||
|
||||
def search_object(request):
|
||||
if request.method == 'POST':
|
||||
classname = request.POST['classname']
|
||||
searchstring = request.POST['searchstring']
|
||||
try:
|
||||
target_leo = dyncode.Object.name2class(classname)
|
||||
except LeApiError:
|
||||
raise HttpException(400)
|
||||
# TODO The get method must be implemented here
|
||||
return get_response('admin/admin_search.html', my_classes = dyncode.dynclasses)
|
||||
|
||||
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
from ..template.loader import TemplateLoader
|
||||
|
||||
# This module contains the web UI controllers that will be called from the web ui class
|
||||
|
||||
##@brief Render a template and return a respone
|
||||
#@param tpl str : template relativ path
|
||||
#@param tpl_vars : templates variables (obsolete)
|
||||
#@param mimetype
|
||||
#@param status_code
|
||||
#@param **kwargs : new version of tpl_vars
|
||||
#@return a response...
|
||||
def get_response(tpl='empty.html', tpl_vars={}, mimetype='text/html', status_code=200, **kwargs):
|
||||
tpl_vars.update(kwargs)
|
||||
loader = TemplateLoader()
|
||||
response = Response(loader.render_to_response(tpl, template_vars=tpl_vars), mimetype=mimetype)
|
||||
response.status_code = status_code
|
||||
return response
|
||||
|
||||
## @brief gets the html template corresponding to a given component type
|
||||
# @param type str : name of the component type
|
||||
# @param params dict : extra parameters to customize the template
|
||||
def get_component_html(type='text', params={}):
|
||||
params['type'] = type
|
||||
template_loader = TemplateLoader()
|
||||
return template_loader.render_to_html(template_file='components/components.html', template_vars=params)
|
||||
|
||||
def index(request):
|
||||
return get_response('index/index.html')
|
||||
|
||||
|
||||
def not_found(request):
|
||||
return get_response('errors/404.html', status_code=404)
|
||||
|
||||
|
||||
def test(request):
|
||||
if 'id' not in request.url_args:
|
||||
id = None
|
||||
else:
|
||||
id = request.url_args['id']
|
||||
|
||||
template_vars = {
|
||||
'id': id,
|
||||
'params': request.GET
|
||||
}
|
||||
return get_response('test.html', tpl_vars=template_vars)
|
||||
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
from .base import get_response
|
||||
|
||||
|
||||
def show_document(request):
|
||||
template_vars = {'id': request.url_args['id']}
|
||||
return get_response('documents/show.html', tpl_vars=template_vars)
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {'lodel.logger': 'logger'})
|
||||
|
||||
from .base import get_response
|
||||
from ...exceptions import *
|
||||
import leapi_dyncode as dyncode
|
||||
|
||||
##@brief These functions are called by the rules defined in ../urls.py
|
||||
## To browse the editorial model
|
||||
|
||||
##@brief Controller's function to list all types (classes) of the editorial model
|
||||
# @param request : the request (get or post)
|
||||
# @note the response is given in a html page called in get_response_function
|
||||
def list_classes(request):
|
||||
if 'allclasses' in request.GET:
|
||||
allclasses = request.GET['allclasses']
|
||||
else:
|
||||
allclasses = 1
|
||||
return get_response('listing/list_classes.html', my_classes=dyncode.dynclasses, allclasses = allclasses)
|
||||
|
||||
##@brief Controller's function to display a type (class) of the editorial model
|
||||
# @param request : the request (get or post)
|
||||
# @note the response is given in a html page called in get_response_function
|
||||
def show_class(request):
|
||||
if 'classname' in request.GET:
|
||||
classname = request.GET['classname']
|
||||
if len(classname) > 1:
|
||||
raise HttpException(400)
|
||||
classname = classname[0]
|
||||
try:
|
||||
target_leo = dyncode.Object.name2class(classname)
|
||||
except LeApiError:
|
||||
classname = None
|
||||
else:
|
||||
raise HttpException(400)
|
||||
return get_response('listing/show_class.html', classname=classname)
|
||||
|
||||
##@brief Controller's function to display an instance or a certain type
|
||||
# @param request : the request (get or post)
|
||||
# @note the response is given in a html page called in get_response_function
|
||||
def show_object(request):
|
||||
if 'classname' in request.GET:
|
||||
classname = request.GET['classname']
|
||||
if len(classname) > 1:
|
||||
raise HttpException(400)
|
||||
classname = classname[0]
|
||||
try:
|
||||
target_leo = dyncode.Object.name2class(classname)
|
||||
except LeApiError:
|
||||
classname = None
|
||||
else:
|
||||
raise HttpException(400)
|
||||
|
||||
logger.warning('Composed uids broken here')
|
||||
uid_field = target_leo.uid_fieldname()[0]
|
||||
|
||||
test_valid = 'lodel_id' in request.GET \
|
||||
and len(request.GET['lodel_id']) == 1
|
||||
|
||||
if test_valid:
|
||||
try:
|
||||
dh = target_leo.field(uid_field)
|
||||
lodel_id = dh.cast_type(request.GET['lodel_id'][0])
|
||||
except (ValueError, TypeError):
|
||||
test_valid = False
|
||||
|
||||
if not test_valid:
|
||||
raise HttpException(400)
|
||||
else:
|
||||
query_filters = list()
|
||||
query_filters.append((uid_field,'=',lodel_id))
|
||||
obj = target_leo.get(query_filters)
|
||||
if len(obj) == 0:
|
||||
raise HttpException(404)
|
||||
return get_response('listing/show_object.html', lodel_id=lodel_id, classname=classname)
|
||||
|
||||
##@brief Controller's function to display an instance or a certain type
|
||||
# @param request : the request (get or post)
|
||||
# @note the response is given in a html page called in get_response_function
|
||||
def show_object_detailled(request):
|
||||
if 'classname' in request.GET:
|
||||
classname = request.GET['classname']
|
||||
if len(classname) > 1:
|
||||
raise HttpException(400)
|
||||
classname = classname[0]
|
||||
try:
|
||||
target_leo = dyncode.Object.name2class(classname)
|
||||
except LeApiError:
|
||||
classname = None
|
||||
else:
|
||||
raise HttpException(400)
|
||||
|
||||
logger.warning('Composed uids broken here')
|
||||
uid_field = target_leo.uid_fieldname()[0]
|
||||
|
||||
test_valid = 'lodel_id' in request.GET \
|
||||
and len(request.GET['lodel_id']) == 1
|
||||
|
||||
if test_valid:
|
||||
try:
|
||||
dh = target_leo.field(uid_field)
|
||||
lodel_id = dh.cast_type(request.GET['lodel_id'][0])
|
||||
except (ValueError, TypeError):
|
||||
test_valid = False
|
||||
|
||||
if not test_valid:
|
||||
raise HttpException(400)
|
||||
else:
|
||||
query_filters = list()
|
||||
query_filters.append((uid_field,'=',lodel_id))
|
||||
obj = target_leo.get(query_filters)
|
||||
if len(obj) == 0:
|
||||
raise HttpException(404)
|
||||
|
||||
return get_response('listing/show_object_detailled.html', lodel_id=lodel_id, classname=classname)
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from .base import get_response
|
||||
from ...exceptions import *
|
||||
from ...client import WebUiClient as WebUiClient
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {'lodel.logger': 'logger'})
|
||||
|
||||
import leapi_dyncode as dyncode #TODO : handle this with context
|
||||
|
||||
##@brief These functions are called by the rules defined in ../urls.py
|
||||
## Their goal is to handle the user authentication
|
||||
|
||||
##@brief Controller's function to login a user, the corresponding form is in interface/users
|
||||
# @param request : the request (get or post)
|
||||
# @note the response is given in a html page called in get_response_function
|
||||
def signin(request):
|
||||
msg=''
|
||||
# The form send the login and password, we can authenticate the user
|
||||
if request.method == 'POST':
|
||||
login = request.form['inputLogin']
|
||||
WebUiClient.authenticate(login, request.form['inputPassword'])
|
||||
# We get the informations about the user
|
||||
uid=WebUiClient['__auth_user_infos']['uid']
|
||||
leoclass=WebUiClient['__auth_user_infos']['leoclass']
|
||||
query_filter=list()
|
||||
query_filter.append((leoclass.uid_fieldname()[0],'=', uid))
|
||||
user = leoclass.get(query_filter)
|
||||
return get_response('users/welcome.html', username = user[0].data('login'))
|
||||
else:
|
||||
return get_response('users/signin.html')
|
||||
|
||||
##@brief Controller's function to logout a user
|
||||
# @param request : the request (get or post)
|
||||
# @note the response is given in the login html page
|
||||
def signout(request):
|
||||
WebUiClient.destroy()
|
||||
return get_response('users/signin.html')
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
from werkzeug.wrappers import Request
|
||||
from werkzeug.urls import url_decode
|
||||
|
||||
|
||||
class LodelRequest(Request):
|
||||
|
||||
def __init__(self, environ):
|
||||
super().__init__(environ)
|
||||
self.PATH = self.path.lstrip('/')
|
||||
self.FILES = self.files.to_dict(flat=False)
|
||||
self.GET = url_decode(self.query_string).to_dict(flat=False)
|
||||
self.POST = self.form.to_dict(flat=False)
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
|
||||
from .controllers import *
|
||||
from .urls import urls
|
||||
from ..main import root_url
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.settings': ['Settings']})
|
||||
|
||||
def format_url_rule(url_rule):
|
||||
if url_rule.startswith('^'):
|
||||
res = url_rule.replace('^', '^'+root_url())
|
||||
else:
|
||||
res = root_url()+'.*'+url_rule
|
||||
return res
|
||||
|
||||
|
||||
def get_controller(request):
|
||||
|
||||
url_rules = []
|
||||
for url in urls:
|
||||
url_rules.append((format_url_rule(url[0]), url[1]))
|
||||
|
||||
# Returning the right controller to call
|
||||
for regex, callback in url_rules:
|
||||
p = re.compile(regex)
|
||||
m = p.search(request.PATH)
|
||||
if m is not None:
|
||||
request.url_args = m.groupdict()
|
||||
return callback
|
||||
|
||||
return not_found
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Lodel 2 templates API : loaded by default
|
||||
|
||||
class Test(object):
|
||||
|
||||
def ok(self):
|
||||
return 'ok'
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class NotAllowedCustomAPIKeyError(Exception):
|
||||
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import jinja2
|
||||
import os
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {'lodel.settings': ['Settings']})
|
||||
|
||||
from ...client import WebUiClient as WebUiClient
|
||||
import leapi_dyncode
|
||||
|
||||
from .api import api_lodel_templates
|
||||
from .exceptions.not_allowed_custom_api_key_error import NotAllowedCustomAPIKeyError
|
||||
from ...main import root_url as root_url
|
||||
from ...main import static_url as static_url
|
||||
from ...main import PLUGIN_PATH
|
||||
TEMPLATE_PATH = os.path.realpath(os.path.join(PLUGIN_PATH, 'templates/'))
|
||||
|
||||
class TemplateLoader(object):
|
||||
|
||||
_reserved_template_keys = ['lodel']
|
||||
|
||||
## @brief Initializes a template loader
|
||||
#
|
||||
# @param search_path str : the base path from which the templates are searched. To use absolute paths, you can set
|
||||
# it to the root "/". By default, it will be the root of the project, defined in the settings of the application.
|
||||
# @param follow_links bool : indicates whether or not to follow the symbolic links (default: True)
|
||||
# @param is_cache_active bool : indicates whether or not the cache should be activated or not (default: True)
|
||||
# @todo connect this to the new settings system
|
||||
def __init__(self, search_path=TEMPLATE_PATH, follow_links=True, is_cache_active=True):
|
||||
self.search_path = search_path
|
||||
self.follow_links = follow_links
|
||||
self.is_cache_active = is_cache_active
|
||||
|
||||
## @brief Renders a HTML content of a template
|
||||
#
|
||||
# @see template.loader.TemplateLoader.render_to_response
|
||||
#
|
||||
# @return str. String containing the HTML output of the processed templated
|
||||
def render_to_html(self, template_file, template_vars={}, template_extra=None):
|
||||
loader = jinja2.FileSystemLoader(searchpath=self.search_path)
|
||||
environment = jinja2.Environment(loader=loader) if self.is_cache_active else jinja2.Environment(loader=loader,
|
||||
cache_size=0)
|
||||
template = environment.get_template(template_file)
|
||||
|
||||
# lodel2 default api is loaded
|
||||
# TODO change this if needed
|
||||
template.globals['lodel'] = api_lodel_templates
|
||||
template.globals['leapi'] = leapi_dyncode
|
||||
template.globals['settings'] = Settings
|
||||
template.globals['client'] = WebUiClient
|
||||
template.globals['root_url'] = root_url()
|
||||
template.globals['static_url'] = static_url()
|
||||
template.globals['url'] = lambda sufix='': root_url()\
|
||||
+ ('' if sufix.startswith('/') else '/')\
|
||||
+ sufix
|
||||
|
||||
# Extra modules are loaded
|
||||
if template_extra is not None:
|
||||
for extra in template_extra:
|
||||
if not self._is_allowed_template_key(extra[0]):
|
||||
raise NotAllowedCustomAPIKeyError("The name '%s' is a reserved one for the loaded APIs in "
|
||||
"templates" % extra[0])
|
||||
template.globals[extra[0]] = extra[1]
|
||||
|
||||
return template.render(template_vars)
|
||||
|
||||
## @brief Renders a template into an encoded form ready to be sent to a wsgi response
|
||||
#
|
||||
# @param template_file str : path to the template file (starting from the base path used to instanciate the
|
||||
# TemplateLoader)
|
||||
# @param template_vars dict : parameters to be used in the template
|
||||
# @param template_extra list : list of tuples indicating the custom modules to import in the template
|
||||
# (default: None).
|
||||
#
|
||||
# The modules are given as tuples with the format : ('name_to_use_in_the_template', module)
|
||||
#
|
||||
# @return str
|
||||
def render_to_response(self, template_file, template_vars={}, template_extra=None):
|
||||
return self.render_to_html(template_file=template_file, template_vars=template_vars,
|
||||
template_extra=template_extra).encode()
|
||||
|
||||
## @brief Checks if the key used for the template is allowed
|
||||
#
|
||||
# @param key str
|
||||
# @return bool
|
||||
def _is_allowed_template_key(self, key):
|
||||
return False if key in self.__class__.__reserved_template_keys else True
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from .controllers import *
|
||||
|
||||
urls = (
|
||||
(r'^/?$', index),
|
||||
(r'^/admin/?$', admin),
|
||||
(r'^/admin/create$', admin_create),
|
||||
(r'^/admin/update$', admin_update),
|
||||
(r'^/admin/delete$', admin_delete),
|
||||
(r'^/admin/classes_admin', admin_classes),
|
||||
(r'^/admin/object_create', create_object),
|
||||
(r'^/admin/object_delete', delete_object),
|
||||
(r'^/admin/class_admin$', admin_class),
|
||||
(r'^/admin/class_delete$', delete_in_class),
|
||||
(r'^/admin/search$', search_object),
|
||||
(r'/test/(?P<id>.*)$', test),
|
||||
(r'^/test/?$', test),
|
||||
(r'^/list_classes', list_classes),
|
||||
(r'^/list_classes?$', list_classes),
|
||||
(r'^/show_object?$', show_object),
|
||||
(r'^/show_object_detailled?$', show_object_detailled),
|
||||
(r'^/show_class?$', show_class),
|
||||
(r'^/signin', signin),
|
||||
(r'^/signout', signout)
|
||||
)
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
import os, os.path
|
||||
import sys
|
||||
import shlex
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.plugin': ['LodelHook'],
|
||||
'lodel.settings': ['Settings']})
|
||||
|
||||
from lodel import buildconf #<-- This one is common to the build
|
||||
|
||||
PLUGIN_PATH = os.path.dirname(__file__)
|
||||
|
||||
##@brief Return the root url of the instance
|
||||
#@warning no trailing slash
|
||||
def root_url():
|
||||
return Settings.sitename
|
||||
|
||||
def static_url():
|
||||
return Settings.webui.static_url
|
||||
|
||||
##@brief uwsgi startup demo
|
||||
@LodelHook('lodel2_loader_main')
|
||||
def uwsgi_fork(hook_name, caller, payload):
|
||||
|
||||
standalone = Settings.webui.standalone
|
||||
if standalone.lower() == 'false':
|
||||
return
|
||||
else:
|
||||
sockfile = os.path.join(buildconf.LODEL2VARDIR, 'uwsgi_sockets/')
|
||||
if not os.path.isdir(sockfile):
|
||||
os.mkdir(sockfile)
|
||||
sockfile = os.path.join(sockfile,
|
||||
Settings.sitename.replace('/','_') + '.sock')
|
||||
logfile = os.path.join(
|
||||
buildconf.LODEL2LOGDIR, 'uwsgi_%s.log' % (
|
||||
Settings.sitename.replace('/', '_')))
|
||||
|
||||
if standalone.lower() == 'true':
|
||||
cmd='{uwsgi} --plugin python3 --http-socket {addr}:{port} --module \
|
||||
plugins.webui.run --socket {sockfile} --logto {logfile} -p {uwsgiworkers}'
|
||||
cmd = cmd.format(
|
||||
addr = Settings.webui.listen_address,
|
||||
port = Settings.webui.listen_port,
|
||||
uwsgi= Settings.webui.uwsgicmd,
|
||||
sockfile=sockfile,
|
||||
logfile = logfile,
|
||||
uwsgiworkers = Settings.webui.uwsgi_workers)
|
||||
if Settings.webui.virtualenv is not None:
|
||||
cmd += " --virtualenv %s" % Settings.webui.virtualenv
|
||||
|
||||
elif Settings.webui.standalone == 'uwsgi':
|
||||
cmd = '{uwsgi} --plugin python3 --ini ./plugins/webui/uwsgi/uwsgi.ini \
|
||||
--socket {sockfile} --logto {logfile} -p {uwsgiworkers}'
|
||||
cmd = cmd.format(uwsgi = Settings.webui.uwsgicmd,
|
||||
sockfile = sockfile, logfile = logfile, uwsgiworkers=Settings.webui.uwsgi_workers)
|
||||
|
||||
try:
|
||||
args = shlex.split(cmd)
|
||||
exit(os.execl(args[0], *args))
|
||||
except Exception as e:
|
||||
print("Webui plugin uwsgi execl fails cmd was '%s' error : " % cmd,
|
||||
e, file=sys.stderr)
|
||||
exit(1)
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import loader # Lodel2 loader
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import time
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from lodel.context import LodelContext
|
||||
LodelContext.expose_modules(globals(), {
|
||||
'lodel.settings': ['Settings'],
|
||||
'lodel.auth.exceptions': ['ClientError', 'ClientAuthenticationFailure',
|
||||
'ClientPermissionDenied', 'ClientAuthenticationError']})
|
||||
|
||||
from .interface.router import get_controller
|
||||
from .interface.lodelrequest import LodelRequest
|
||||
from .exceptions import *
|
||||
from .client import WebUiClient
|
||||
|
||||
try:
|
||||
SESSION_FILES_BASE_DIR = Settings.webui.sessions.directory
|
||||
SESSION_FILES_TEMPLATE = Settings.webui.sessions.file_template
|
||||
SESSION_EXPIRATION_LIMIT = Settings.webui.sessions.expiration
|
||||
|
||||
|
||||
COOKIE_SECRET_KEY = bytes(Settings.webui.cookie_secret_key, 'utf-8')
|
||||
COOKIE_SESSION_ID = Settings.webui.cookie_session_id
|
||||
except Exception as e:
|
||||
print("Fails to start : ", e, file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
from werkzeug.contrib.securecookie import SecureCookie
|
||||
|
||||
def load_cookie(request):
|
||||
datas = request.cookies.get(COOKIE_SESSION_ID)
|
||||
|
||||
if not datas:
|
||||
return None
|
||||
|
||||
cookie_content = SecureCookie.unserialize(datas, COOKIE_SECRET_KEY)
|
||||
|
||||
if 'token' not in cookie_content:
|
||||
return None
|
||||
|
||||
token = cookie_content['token']
|
||||
|
||||
if token is None or len(token) == 0:
|
||||
return None
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def save_cookie(response, token):
|
||||
response.set_cookie(COOKIE_SESSION_ID, SecureCookie({'token': token}, COOKIE_SECRET_KEY).serialize())
|
||||
|
||||
|
||||
def empty_cookie(response):
|
||||
response.set_cookie(COOKIE_SESSION_ID, '')
|
||||
|
||||
#Starting instance
|
||||
loader.start()
|
||||
#providing access to dyncode
|
||||
|
||||
##@todo Dirty & quick dyncode access providing. Replace it by a clean access
|
||||
#using LodelContext
|
||||
lodel = LodelContext.get()
|
||||
import leapi_dyncode as dyncode
|
||||
lodel.dyncode = dyncode
|
||||
|
||||
|
||||
# WSGI Application
|
||||
def application(env, start_response):
|
||||
request = LodelRequest(env)
|
||||
session_token = None
|
||||
try:
|
||||
#We have to create the client before restoring cookie in order to be able
|
||||
#to log messages with client infos
|
||||
client = WebUiClient(env['REMOTE_ADDR'], env['HTTP_USER_AGENT'], None)
|
||||
session_token = load_cookie(request)
|
||||
if session_token is not None and len(session_token) > 0:
|
||||
WebUiClient.restore_session(session_token)
|
||||
session_token = None
|
||||
|
||||
try:
|
||||
controller = get_controller(request)
|
||||
logger.debug(controller)
|
||||
response = controller(request)
|
||||
except HttpException as e:
|
||||
try:
|
||||
response = e.render(request)
|
||||
except Exception as eb:
|
||||
raise eb
|
||||
res = Response()
|
||||
res.status_code = 500
|
||||
return res
|
||||
session_token = WebUiClient.session_token()
|
||||
if session_token is not None:
|
||||
save_cookie(response, session_token)
|
||||
session_token = None
|
||||
except (ClientError, ClientAuthenticationError):
|
||||
response = HttpException(200).render(request)
|
||||
empty_cookie(response)
|
||||
except ClientAuthenticationFailure:
|
||||
response = HttpException(200).render(request)
|
||||
empty_cookie(response)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
res = response(env, start_response)
|
||||
|
||||
WebUiClient.clean()
|
||||
return res
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{% extends "base_backend.html" %}
|
||||
{% block title %}- Index{% endblock %}
|
||||
{% block body %}
|
||||
<ol class="breadcrumb">
|
||||
<li class="active">Home</li>
|
||||
</ol>
|
||||
<h1 class="h1_lodel">{{settings.sitename}} administration</h1>
|
||||
<ul>
|
||||
<li><a href="admin/classes_admin">Edit an object</a></li>
|
||||
<li><a href="admin/object_create">Create an object</a></li>
|
||||
<li><a href="admin/object_delete">Delete an object</a></li>
|
||||
<li><a href="admin/search">Search an object</a></li>
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{% extends "base_backend.html" %}
|
||||
{% import "admin/editable_component.html" as edit %}
|
||||
|
||||
{% block title %}- Creating a new {{target.__name__}}{% endblock %}
|
||||
{% block body %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/{{ root_url }}/admin">Home</a></li>
|
||||
<li><a href="/{{ root_url }}/admin/object_create">Creation</a></li>
|
||||
<li class="active">{{target.__name__}}</li>
|
||||
</ol>
|
||||
{% if msg is not none %}
|
||||
{% block msg %} <p style="color:red; font-size:20pt; font-weight:bold">{{ msg }}</p> {% endblock %}
|
||||
{% endif %}
|
||||
<h1 class="h1_lodel">Creating a new {{target.__name__}}</h1>
|
||||
<form class="form-horizontal" action="" method ="post">
|
||||
<input type="hidden" name="classname" id="classname" value="{{target.__name__}}" />
|
||||
{% for fieldname, field in target.fields().items() %}
|
||||
<div class="form-group">
|
||||
<div class="form-group" style="padding-bottom:15px;"> {{edit.input(fieldname, field) }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<p> </p>
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a class="btn btn-default" href="object_create">Return</a>
|
||||
</form>
|
||||
<div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
{% extends "base_backend.html" %}
|
||||
|
||||
{% block title %}- Suppression in {{target.__name__}}{% endblock %}
|
||||
{% block body %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/{{ root_url }}/admin">Home</a></li>
|
||||
<li><a href="/{{ root_url }}/admin/object_create">Suppression</a></li>
|
||||
<li class="active">{{target.__name__}}</li>
|
||||
</ol>
|
||||
{% if msg is not none %}
|
||||
{% block msg %} <p style="color:red; font-size:20pt; font-weight:bold">{{ msg }}</p> {% endblock %}
|
||||
{% endif %}
|
||||
<h1 class="h1_lodel">Remove a {{target.__name__}} object</h1>
|
||||
<div style="text-align:center"> <a class="btn btn-default" href="object_delete">Return</a></div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
{% extends "base_backend.html" %}
|
||||
{% import "admin/editable_component.html" as edit %}
|
||||
{% set uidfield = target.uid_fieldname()[0] %}
|
||||
{% set objects = target.get(('%s = %s') % (uidfield, lodel_id)) %}
|
||||
{% set obj = objects.pop() %}
|
||||
{% block title %}Edit Object{% endblock %}
|
||||
{% block body %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/{{ root_url }}/admin">Home</a></li>
|
||||
<li><a href="/{{ root_url }}/admin/classes_admin">Edition</a></li>
|
||||
<li><a href="/{{ root_url }}/admin/class_admin?classname={{ target.__name__ }}">{{ target.__name__ }}</a></li>
|
||||
<li class="active">Edit</li>
|
||||
</ol>
|
||||
{% if msg is not none %}
|
||||
{% block msg %} <p style="color:red; font-size:20pt; font-weight:bold">{{ msg }}</p> {% endblock %}
|
||||
{% endif %}
|
||||
<h1 class="h1_lodel">Lodel 2 - Edit {{ target.__name__ }} with uid {{ lodel_id }} </h1>
|
||||
<form class="form-horizontal" action="" method ="post">
|
||||
<input type="hidden" name="uid" value="{{ lodel_id}}" />
|
||||
<input type="hidden" name="classname" value={{ target.__name__ }} />
|
||||
{% for fieldname, fieldvalue in obj.fields().items() %}
|
||||
<div class="form-group">
|
||||
<div style="padding-bottom:15px;"> {{edit.input(fieldname, fieldvalue, obj.data(fieldname)) }} </div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<a class="btn btn-default" href="classes_admin">Return</a>
|
||||
</form>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
{% extends "base_backend.html" %}
|
||||
|
||||
{% block title %}- Search{% endblock %}
|
||||
{% block body %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/{{ root_url }}/admin">Home</a></li>
|
||||
<li><a href="/{{ root_url }}/admin/object_create">Search</a></li>
|
||||
</ol>
|
||||
{% if msg is not none %}
|
||||
{% block msg %} <p style="color: red; font-size:20pt; font-weight:bold">{{ msg }}</p> {% endblock %}
|
||||
{% endif %}
|
||||
<h1 class="h1_lodel">Searching</h1>
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
<select id="classname" name="classname">
|
||||
{% for classe in my_classes %}
|
||||
{% if not classe.is_abstract()%}
|
||||
<option value="{{ classe.__name__ }}">{{ classe.__name__ }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="text" name="searchstring" id="searchstring" value="" />
|
||||
<p> </p>
|
||||
<button type="submit" class="btn btn-primary">Search</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{% macro input(fieldname, field, value='') -%}
|
||||
<label for="field_input_{{fieldname}}" class="col-sm-2 control-label">{{fieldname}}</label>
|
||||
<div class="col-xs-6">
|
||||
{% if value == None %}
|
||||
{% set value = '' %}
|
||||
{% endif %}
|
||||
{% if field.base_type == 'bool' %}
|
||||
<input id="field_input_{{fieldname}}" class="form-control" name="field_input_{{fieldname}}" type="checkbox" checked="{% if value %}checked{% endif %}" >
|
||||
{% elif field.base_type == 'password' %}
|
||||
<input id="{{fieldname}}" name="field_input_{{fieldname}}" class="form-control" type="password" value="{{sval}}" >
|
||||
{% elif field.base_type == 'char' or field.base_type == 'int' %}
|
||||
<input id="{{fieldname}}" class="form-control" name="field_input_{{fieldname}}" type="text" value="{{value}}" >
|
||||
{% elif field.base_type == 'ref' %}
|
||||
{% if value is iterable %}
|
||||
{% set sval=value|join(',') %}
|
||||
{% else %}
|
||||
{% set sval = value %}
|
||||
{% endif %}
|
||||
{% if field.directly_editable %}
|
||||
<input id="{{fieldname}}" class="form-control" name="field_input_{{fieldname}}" type="text" value="{{sval}}" >
|
||||
{% set l_classe = field.allowed_classes %}
|
||||
<p> Please enter uids to instances of {{ l_classe.__name__ }} separated by commas </p>
|
||||
{% else %}
|
||||
<input id="{{fieldname}}" class="form-control" name="field_input_{{fieldname}}" type="text" value="{{sval}}">
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Unsupported base type "{{field.base_type}}" <br>
|
||||
{% endif %}
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
{% extends "base_backend.html" %}
|
||||
{% block title %}Lodel 2 - Admin - List of Classes{% endblock %}
|
||||
{% block body %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/{{ root_url }}/admin">Home</a></li>
|
||||
<li class="active">Edit</li>
|
||||
</ol>
|
||||
<h1 class="h1_lodel">Edition</h1>
|
||||
<h2>Select a class</h2>
|
||||
<ul>
|
||||
{% for classe in my_classes %}
|
||||
{% set abst = ' - Abstract' %}
|
||||
{% if not classe.is_abstract() %}
|
||||
{% set abst = ' - ' ~ classe.get(None)|length %}
|
||||
<li> <a href="/{{ root_url }}/admin/class_admin?classname={{ classe.__name__ }}" >{{ classe.__name__ }} </a>{{ abst }}</li>
|
||||
{% else %}
|
||||
<li> {{ classe.__name__ }} {{ abst }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div>
|
||||
<a href="../admin/">Return</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
{% extends "base_backend.html" %}
|
||||
{% block title %}Lodel 2 - Admin - List of Classes{% endblock %}
|
||||
{% block body %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/{{ root_url }}/admin">Home</a></li>
|
||||
<li class="active">Creation</li>
|
||||
</ol>
|
||||
<h1 class="h1_lodel">Creation</h1>
|
||||
<h2>Choose a type to create an instance</h1>
|
||||
<ul>
|
||||
{% for classe in my_classes %}
|
||||
{% set abst = '' %}
|
||||
{% if not classe.is_abstract() %}
|
||||
<li> <a href="create?classname={{ classe.__name__ }}" >{{ classe.__name__ }} </a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div>
|
||||
<a href="{{ root_url }}/admin/">Return</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
{% extends "base_backend.html" %}
|
||||
{% block title %}Lodel 2 - Admin - List of Classes{% endblock %}
|
||||
{% block body %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/{{ root_url }}/admin">Home</a></li>
|
||||
<li class="active">Deletion</li>
|
||||
</ol>
|
||||
<h1 class="h1_lodel">Deletion</h1>
|
||||
<h2>Choose a type to delete an instance</h1>
|
||||
<ul>
|
||||
{% for classe in my_classes %}
|
||||
{% set abst = '' %}
|
||||
{% if not classe.is_abstract() %}
|
||||
<li> <a href="class_delete?classname={{ classe.__name__ }}" >{{ classe.__name__ }} </a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div>
|
||||
<a href="{{ root_url }}/admin/">Return</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{% extends "base_backend.html" %}
|
||||
{% block title %}Lodel 2 - Admin - Class {{ target.__name__ }} {% endblock %}
|
||||
{% block body %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/{{ root_url }}/admin">Home</a></li>
|
||||
<li><a href="/{{ root_url }}/admin/classes_admin">Edition</a></li>
|
||||
<li class="active">{{target.__name__ }}</li>
|
||||
</ol>
|
||||
<h1 class="h1_lodel">Edition - {{target.__name__ }} </h1>
|
||||
|
||||
{% if not target.is_abstract() %}
|
||||
{% set objects = target.get(None) %}
|
||||
<ul>
|
||||
{% for obj in objects %}
|
||||
<li><a href="/{{ root_url }}/admin/update?classname={{ target.__name__ }}&lodel_id={{ obj.uid() }}" >{{ obj.uid() }} </a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{% extends "base_backend.html" %}
|
||||
{% block title %}Lodel 2 - Admin - Class {{ target.__name__ }} {% endblock %}
|
||||
{% block body %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/{{ root_url }}/admin">Home</a></li>
|
||||
<li><a href="/{{ root_url }}/admin/classes_admin">Deletion</a></li>
|
||||
<li class="active">{{target.__name__ }}</li>
|
||||
</ol>
|
||||
<h1 class="h1_lodel">Deletion - {{target.__name__ }} </h1>
|
||||
|
||||
{% if not target.is_abstract() %}
|
||||
{% set objects = target.get(None) %}
|
||||
<ul>
|
||||
{% for obj in objects %}
|
||||
<li><a href="delete?classname={{ target.__name__ }}&lodel_id={{ obj.uid() }}" >{{ obj.uid() }} </a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
<!doctype html>
|
||||
{% set not_connected = false %} <!-- client.is_anonymous() %} -->
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
|
||||
<!-- Latest compiled and minified CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||
<!--{{url('/')}} -->
|
||||
<!-- Optional theme -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
|
||||
<link href="{{ static_url }}/css/template.css" rel="stylesheet">
|
||||
{% block style %}{% endblock %}
|
||||
{% block scripts %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<!-- Fixed navbar -->
|
||||
<nav class="navbar navbar-default navbar-fixed-top">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/{{ root_url }}/">Lodel 2</a>
|
||||
</div>
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="active"><a href="/{{ root_url }}/">Home</a></li>
|
||||
<li><a href="list_classes">All types</a></li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{% if not_connected %}
|
||||
<li id="backend-nav"><a href="/{{ root_url }}/admin" class="btn btn-link disabled">Back-end</a></li>
|
||||
<li id="signin-nav"><a href="signin">Sign In</a></li> -->
|
||||
{% else %}
|
||||
<li id="backend-nav"><a href="/{{ root_url }}/admin" class="btn btn-link">Back-end</a></li>
|
||||
<li id="signout-nav"><a href="signout">Logout</a>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div><!--/.nav-collapse -->
|
||||
</div>
|
||||
</nav>
|
||||
<div id="content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
<script type="text/javascript">{% block javascript %}{% endblock %}</script>
|
||||
|
||||
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
|
||||
<!-- Include all compiled plugins (below), or include individual files as needed -->
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>{{ settings.sitename }} Admin{% block title %}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
|
||||
<link href="{{ static_url }}/css/template.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||
{% block style %}
|
||||
{% endblock %}
|
||||
{% block scripts %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<!-- Fixed navbar -->
|
||||
<nav class="navbar navbar-default navbar-fixed-top">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="#">Lodel 2 Administration</a>
|
||||
</div>
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="active"><a href="/{{ root_url }}/admin">Home</a></li>
|
||||
<li><a href="/{{ root_url }}/admin/object_create">Create</a></li>
|
||||
<li><a href="/{{ root_url }}/admin/classes_admin">Edit</a></li>
|
||||
<li><a href="/{{ root_url }}/admin/object_delete">Delete</a></li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li><a href="/{{ root_url }}/">Front-End</a></li>
|
||||
<li id="signout-nav"><a href="/{{ root_url }}/signout">Logout</a>
|
||||
</ul>
|
||||
</div><!--/.nav-collapse -->
|
||||
</div>
|
||||
</nav>
|
||||
{% block body %}{% endblock %}
|
||||
<script type="text/javascript">{% block javascript %}{% endblock %}</script>
|
||||
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
|
||||
<!-- Include all compiled plugins (below), or include individual files as needed -->
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{% macro input(name, value='', type='text') -%}
|
||||
<input type="{{ type }}" value="{{ value }}" name="{{ name }}" id= "{{ name }}"/>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro textarea(name, value='', rows=10, cols=40) -%}
|
||||
<textarea name="{{ name }}" rows="{{ rows }}" cols="{{ cols }}">
|
||||
{{ value|e }}
|
||||
</textarea>
|
||||
{%- endmacro %}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
body {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.form-signin {
|
||||
max-width: 330px;
|
||||
padding: 15px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.form-signin .form-signin-heading,
|
||||
.form-signin .checkbox {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.form-signin .checkbox {
|
||||
font-weight: normal;
|
||||
}
|
||||
.form-signin .form-control {
|
||||
position: relative;
|
||||
height: auto;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.form-signin .form-control:focus {
|
||||
z-index: 2;
|
||||
}
|
||||
.form-signin input[type="email"] {
|
||||
margin-bottom: -1px;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.form-signin input[type="password"] {
|
||||
margin-bottom: 10px;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
body {
|
||||
min-height: 2000px;
|
||||
padding-top: 70px;
|
||||
padding-left: 70px;
|
||||
}
|
||||
|
||||
h1{
|
||||
padding-bottom : 30px;
|
||||
}
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{% extends "base_backend.html" %}
|
||||
{% block title %}Lodel 2 - Document {{ id }}{% endblock %}
|
||||
{% block content %}
|
||||
{{ leapi.Section.get(['lodel_id = %s' % id]) }}
|
||||
{% endblock %}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{status_code}} {{status_str}}</title>
|
||||
</head>
|
||||
<body>
|
||||
{{status_code}} {{status_str}}
|
||||
<hr/>
|
||||
{{custom}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Error 404{% endblock %}
|
||||
{% block content %}
|
||||
<h3>404 - File Not Found</h3>
|
||||
{% endblock %}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Lodel 2 - DASHBOARD{% endblock %}
|
||||
{% block content %}
|
||||
<ul>
|
||||
<li><a href="list_classes">Tous les types</a></li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
{% macro display(obj) -%}
|
||||
<!-- To get a component HTML code, it is necessary to call : components.<macro_name>(args) -->
|
||||
{% for fieldn, fieldv in obj.fields(include_ro = True).items() %}
|
||||
{% if fieldv is not none %}
|
||||
{% if fieldv.base_type == 'ref' %}
|
||||
{% if obj.data(fieldn) is iterable %}
|
||||
<li>{{ fieldn }}
|
||||
{% set l_classe = fieldv.allowed_classes[0] %}
|
||||
<ul>
|
||||
{% for rel in obj.data(fieldn) %}
|
||||
{% set casttype = l_classe.data_handler(l_classe.uid_fieldname()[0]).cast_type %}
|
||||
{% set linked_object = l_classe.get(('%s = %s') % (l_classe.uid_fieldname()[0], rel)) %}
|
||||
{% set rel2 = casttype(rel) %}
|
||||
<li><a href="show_object_detailled?classname={{ l_classe.__name__ }}&lodel_id={{ rel2 }}" >{{ rel2 }}</a></li>
|
||||
{% endfor %}
|
||||
</ul></li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li> {{ fieldn }} : {{ obj.data(fieldn) }} </li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Lodel 2 - List of Classes{% endblock %}
|
||||
{% block content %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/{{ root_url }}/">Home</a></li>
|
||||
<li class="active">Types</li>
|
||||
</ol>
|
||||
<h1 class="h1_lodel">All types</h1>
|
||||
<ul>
|
||||
{% for classe in my_classes %}
|
||||
{% if classe.is_abstract() and allclasses == 1 %}
|
||||
{% set abst = ' - Abstract type ' %}
|
||||
<li> <a href="show_class?classname={{ classe.__name__ }}" >{{ classe.__name__ }} </a>{{ abst }}</li>
|
||||
{% elif not classe.is_abstract() %}
|
||||
{% set abst = ' - ' ~ classe.get(None)|length %}
|
||||
<li> <a href="show_class?classname={{ classe.__name__ }}" >{{ classe.__name__ }} </a>{{ abst }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Lodel 2 - Class {{ classname }} {% endblock %}
|
||||
{% block content %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/{{ root_url }}/">Home</a></li>
|
||||
<li><a href="/{{ root_url }}/list_classes">Types</a></li>
|
||||
<li class="active">{{ classname }}</li>
|
||||
</ol>
|
||||
<h1 class="h1_lodel">Type {{ classname }} </h1>
|
||||
{% set my_class = leapi.name2class(classname) %}
|
||||
{% if my_class.child_classes()|length >0 %}
|
||||
<h2> Childs types</h2>
|
||||
<ul>
|
||||
{% for child in my_class.child_classes() %}
|
||||
{% if child.is_abstract() %}
|
||||
{% set abst = ' - Abstract class ' %}
|
||||
{% else %}
|
||||
{% set abst = ' - ' ~ child.get(None)|length %}
|
||||
{% endif %}
|
||||
<li><a href="/{{ root_url }}/show_class?classname={{ child.__name__ }}" >{{ child.__name__ }}</a>{{ abst }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if not my_class.is_abstract() %}
|
||||
<h2>Instances</h2>
|
||||
{% set uid_f = my_class.uid_fieldname() %}
|
||||
{% set objects = my_class.get(None) %}
|
||||
<ul>
|
||||
{% for obj in objects %}
|
||||
<li><a href="/{{ root_url }}/show_object?classname={{ classname }}&lodel_id={{ obj.uid() }}" >{{ obj.uid() }} </a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% import 'components/components.html' as components %}
|
||||
{% set my_class = leapi.name2class(classname) %}
|
||||
{% set uidfield = my_class.uid_fieldname()[0] %}
|
||||
{% set objects = my_class.get(('%s = %s') % (uidfield, lodel_id)) %}
|
||||
{% set obj = objects.pop() %}
|
||||
{% if my_class.is_abstract() %}
|
||||
{% set classname = obj.data('classname') %}
|
||||
{% set my_class = my_class.name2class(classname) %}
|
||||
{% endif %}
|
||||
{% block title %}Object {{ lodel_id }} {% endblock %}
|
||||
{% import "components/components.html" as components %}
|
||||
{% block content %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/{{ root_url }}/">Home</a></li>
|
||||
<li><a href="/{{ root_url }}/list_classes">Types</a></li>
|
||||
<li><a href="/{{ root_url }}/show_class?classname={{ classname }}">{{ classname }}</a></li>
|
||||
<li class="active">{{ lodel_id }}</li>
|
||||
</ol>
|
||||
<h1 class="h1_lodel">Lodel 2 - {{ classname }} with uid {{ lodel_id }}</h1>
|
||||
<ul>
|
||||
<!-- To get a component HTML code, it is necessary to call : components.<macro_name>(args) -->
|
||||
{% for fieldname, fieldvalue in obj.fields(include_ro = True).items() %}
|
||||
{% if fieldvalue is not none %}
|
||||
{% if fieldvalue.base_type == 'ref' %}
|
||||
{% if obj.data(fieldname) is iterable %}
|
||||
<li>{{ fieldname }}
|
||||
{% set l_classe = fieldvalue.allowed_classes[0] %}
|
||||
<ul>
|
||||
{% for rel in obj.data(fieldname) %}
|
||||
{% set casttype = l_classe.data_handler(l_classe.uid_fieldname()[0]).cast_type %}
|
||||
{% set rel2 = casttype(rel) %}
|
||||
<li><a href="show_object?classname={{ l_classe.__name__ }}&lodel_id={{ rel2 }}" >{{ rel2 }}</a></li>
|
||||
{% endfor %}
|
||||
</ul></li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li> {{ fieldname }} : {{ obj.data(fieldname) }} </li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% import 'components/components.html' as components %}
|
||||
{% import "listing/display_obj.html" as edit %}
|
||||
{% set my_class = leapi.name2class(classname) %}
|
||||
{% set uidfield = my_class.uid_fieldname()[0] %}
|
||||
{% set objects = my_class.get(('%s = %s') % (uidfield, lodel_id)) %}
|
||||
{% set obj = objects.pop() %}
|
||||
{% if my_class.is_abstract() %}
|
||||
{% set classname = obj.data('classname') %}
|
||||
{% set my_class = my_class.name2class(classname) %}
|
||||
{% endif %}
|
||||
{% block title %}Object {{ lodel_id }} {% endblock %}
|
||||
{% block content %}
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/{{ root_url }}/">Home</a></li>
|
||||
<li><a href="/{{ root_url }}/list_classes">Types</a></li>
|
||||
<li><a href="/{{ root_url }}/show_class?classname={{ classname }}">{{ classname }}</a></li>
|
||||
<li class="active">{{ lodel_id }}</li>
|
||||
</ol>
|
||||
<h1 class="h1_lodel">Lodel 2 - {{ classname }} with uid {{ lodel_id }}</h1>
|
||||
<ul>
|
||||
<!-- To get a component HTML code, it is necessary to call : components.<macro_name>(args) -->
|
||||
{% for fieldname, fieldvalue in obj.fields(include_ro = True).items() %}
|
||||
{% if fieldvalue is not none %}
|
||||
{% if fieldvalue.base_type == 'ref' %}
|
||||
{% if obj.data(fieldname) is iterable %}
|
||||
<li>{{ fieldname }}
|
||||
{% set l_classe = fieldvalue.allowed_classes[0] %}
|
||||
<ul>
|
||||
{% set linked_objs=l_classe.get(("%s in (%s)") % (l_classe.uid_fieldname()[0], obj.data(fieldname)|join(','))) %}
|
||||
{% for linked_obj in linked_objs %}
|
||||
{{ edit.display(linked_obj) }}
|
||||
<br/>
|
||||
{% endfor %}
|
||||
</ul></li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li> {{ fieldname }} : {{ obj.data(fieldname) }} </li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
{% import "components/components.html" as components %}
|
||||
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
{{ components.textarea('test', value='ceci est un test', rows=10, cols=20) }}<br/>
|
||||
URL arg : id = {{ id }}<br />
|
||||
GET values :<br />
|
||||
<ul>
|
||||
{% for argument_name, argument_value in params.items() %}
|
||||
<li>{{argument_name}} = {{ argument_value }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<form action="http://localhost:9090/admin?r=1&rand[]=7&rand[]=5" method="POST" enctype="multipart/form-data">
|
||||
<input type="text" name="re[]" value="3"><br />
|
||||
<input type="text" name="re[]" value="1"><br />
|
||||
<input type="text" name="val" value="8"><br />
|
||||
<input type="file" name="myfile1"><br />
|
||||
<input type="file" name="myfile2"><br />
|
||||
<input type="file" name="myfiles[]"><br />
|
||||
<input type="file" name="myfiles[]"><br />
|
||||
<input type="submit" value="tester"><br />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Lodel 2 - Sign In{% endblock %}
|
||||
<!-- Custom styles for this template -->
|
||||
{% block style %}
|
||||
<link href="http://147.94.79.8/css/signin.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1 class="h1_lodel">Lodel2 - Sign In</h1>
|
||||
<form class="form-horizontal" method="POST" action="">
|
||||
<div class="form-group">
|
||||
<label for="inputLogin" class="col-sm-2 control-label">Login</label>
|
||||
<div class="col-xs-4">
|
||||
<input type="text" class="form-control" id="inputLogin" name="inputLogin" placeholder="Login" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputPassword" class="col-sm-2 control-label">Password</label>
|
||||
<div class="col-xs-4">
|
||||
<input type="password" class="form-control" id="inputPassword" name="inputPassword" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox"> Remember me
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-default">Sign in</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div> <!-- /container -->
|
||||
{% endblock %}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Lodel 2 - Welcome{% endblock %}
|
||||
<!-- Custom styles for this template -->
|
||||
{% block style %}
|
||||
<link href="http://147.94.79.8/css/signin.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>Lodel2 - Welcome {{ username }}</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-6">You are successfully login...</div>
|
||||
</div>
|
||||
<div class = "row" style="padding-top:20px;">
|
||||
<a class="btn btn-default" href="/{{ root_url }}/signout" role="button">Logout</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div> <!-- /container -->
|
||||
{% endblock %}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
[uwsgi]
|
||||
model = run:application
|
||||
plugin = python3
|
||||
module = plugins.webui.run
|
||||
chmod-socket = 666
|
||||
vacuum = true
|
||||
die-on-term = true
|
||||
master = true
|
||||
Loading…
Add table
Add a link
Reference in a new issue