1
0
Fork 0
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:
Yann 2016-11-03 17:02:46 +01:00
commit 0406e91846
82 changed files with 28 additions and 8 deletions

View file

@ -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)

View file

@ -1,2 +0,0 @@
##@defgroup lodel2_plugins_list Plugins lodel
#@brief Regroup all implemented plugin documentation

View file

@ -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

View file

@ -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'))
}
}

View file

@ -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__)

View file

@ -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'))}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'))
}
}

View file

@ -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

View file

@ -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])

View file

@ -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....
#

View file

@ -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'))
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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]

View file

@ -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')),
}
}

View file

@ -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)

View file

@ -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')),
}
}

View file

@ -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")

View file

@ -1,5 +0,0 @@
__plugin_name__ = 'webui'
__version__ = '0.0.1'
__plugin_type__ = 'ui'
__loader__ = 'main.py'
__confspec__ = 'confspec.py'

View file

@ -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)

View file

@ -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')),
}
}

View file

@ -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]

View file

@ -1,7 +0,0 @@
from .base import *
from .admin import *
from .document import *
from .listing import *
from .users import *

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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')

View file

@ -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)

View file

@ -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

View file

@ -1,8 +0,0 @@
# -*- coding: utf-8 -*-
# Lodel 2 templates API : loaded by default
class Test(object):
def ok(self):
return 'ok'

View file

@ -1,7 +0,0 @@
#-*- coding: utf-8 -*-
class NotAllowedCustomAPIKeyError(Exception):
def __init__(self, message):
self.message = message

View file

@ -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

View file

@ -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)
)

View file

@ -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)

View file

@ -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

View file

@ -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 %}

View file

@ -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>&nbsp;</p>
<button type="submit" class="btn btn-primary">Save</button>
<a class="btn btn-default" href="object_create">Return</a>
</form>
<div>
</div>
{% endblock %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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>&nbsp;</p>
<button type="submit" class="btn btn-primary">Search</button>
</form>
{% endblock %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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>

View file

@ -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>

View file

@ -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 %}

View file

@ -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;
}

View file

@ -1,10 +0,0 @@
body {
min-height: 2000px;
padding-top: 70px;
padding-left: 70px;
}
h1{
padding-bottom : 30px;
}

View file

@ -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 %}

View file

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>{{status_code}} {{status_str}}</title>
</head>
<body>
{{status_code}} {{status_str}}
<hr/>
{{custom}}
</body>
</html>

View file

@ -1,5 +0,0 @@
{% extends "base.html" %}
{% block title %}Error 404{% endblock %}
{% block content %}
<h3>404 - File Not Found</h3>
{% endblock %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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