From 4383606fbb3d812a0c850555a4cfb7b0879fb411 Mon Sep 17 00:00:00 2001 From: Yann Date: Wed, 2 Dec 2015 14:46:50 +0100 Subject: [PATCH] Write an instance creation script and replace the SQLMigrationHandler by the MysqlMigrationHandler see README.md for more informations about instance creation --- DataSource/MySQL/migrationhandler.py | 659 ++++++++++++++---- .../MySQL/migrationhandler__future__.py | 156 +++++ README.md | 10 + install/Makefile | 17 + install/dynleapi.py | 171 +++++ install/em.json | 579 +++++++++++++++ install/loader.py | 28 + install/settings.py | 21 + install/utils.py | 29 + lodel_init.sh | 47 ++ refreshdyn.py | 8 +- 11 files changed, 1588 insertions(+), 137 deletions(-) create mode 100644 DataSource/MySQL/migrationhandler__future__.py create mode 100644 install/Makefile create mode 100644 install/dynleapi.py create mode 100644 install/em.json create mode 100644 install/loader.py create mode 100644 install/settings.py create mode 100755 install/utils.py create mode 100755 lodel_init.sh diff --git a/DataSource/MySQL/migrationhandler.py b/DataSource/MySQL/migrationhandler.py index b9a4d91..4b696fb 100644 --- a/DataSource/MySQL/migrationhandler.py +++ b/DataSource/MySQL/migrationhandler.py @@ -1,156 +1,547 @@ # -*- coding: utf-8 -*- -## @package EditorialModel.migrationhandler.sql -# @brief A dummy migration handler -# -# According to it every modifications are possible -# +import copy +import pymysql import EditorialModel +from DataSource.MySQL.common_utils import MySQL from DataSource.dummy.migrationhandler import DummyMigrationHandler -from EditorialModel.fieldtypes.generic import GenericFieldType -from EditorialModel.model import Model -from mosql.db import Database -from Lodel.utils.mosql import create, alter_add + +# The global MH algorithm is as follow : +# A create_table(table_name, pk_name, pk_opt) method that create a table +# with one pk field +# An add_column(table_name, field_name, field_opt) method that add a column to a table +# +# The create_default_table method will call both methods to create the object and relation tables +# +# Supported operations : +# - EmClass creation +# - EmClass deletion (untested) +# - EmField creation +# - EmField deletion (untested) +# - rel2type attribute creation +# - rel2type attribute deletion (unstested) +# +# Unsupported operations : +# - EmClass rename +# - EmField rename +# - rel2type field rename +# - rel2type attribute rename +# - EmFieldType changes +# +# @todo Unified datasources and migration handlers via utils functions -## Manage Model changes -class SQLMigrationHandler(DummyMigrationHandler): +## @brief Modify a MySQL database given editorial model changes +class MysqlMigrationHandler(DummyMigrationHandler): - fieldtype_to_sql = { - 'char': "CHAR(255)", - 'integer': 'INT' - } + ## @brief Construct a MysqlMigrationHandler + # @param host str : The db host + # @param user str : The db user + # @param password str : The db password + # @param db str : The db name + def __init__(self, host, user, passwd, db, module=pymysql, db_engine='InnoDB', foreign_keys=True, debug=False, dryrun=False, drop_if_exists=False): + self.datasource = MySQL + self._dbmodule = module + #Connect to MySQL + self.db = self._dbmodule.connect(host=host, user=user, passwd=passwd, db=db) + self.debug = debug + self.dryrun = dryrun + self.db_engine = db_engine + self.foreign_keys = foreign_keys if db_engine == 'InnoDB' else False + self.drop_if_exists = drop_if_exists + #Create default tables + self._create_default_tables(self.drop_if_exists) - def __init__(self, module=None, *conn_args, **conn_kargs): - super(SQLMigrationHandler, self).__init__(False) + ## @brief Delete all table created by the MH + # @param model Model : the Editorial model + def __purge_db(self, model): + for uid in [c.uid for c in model.components('EmClass')]: + try: + self.delete_emclass_table(model, uid) + except self._dbmodule.err.InternalError as e: + print(e) - self.db = Database(module, *conn_args, **conn_kargs) - self._pk_column = (EditorialModel.classtypes.pk_name(), 'INTEGER PRIMARY KEY AUTOINCREMENT') - self._main_table_name = 'object' - self._relation_table_name = 'relation' + for tname in [MySQL.get_r2t2table_name(f.em_class.name, model.component(f.rel_to_type_id).name) for f in model.components('EmField') if f.fieldtype == 'rel2type']: + try: + self._query("DROP TABLE %s;" % tname) + except self._dbmodule.err.InternalError as e: + print(e) - self._install_tables() + for tname in [MySQL.relations_table_name, MySQL.objects_table_name]: + try: + self._query("DROP TABLE %s;" % tname) + except self._dbmodule.err.InternalError as e: + print(e) - ## @brief Record a change in the EditorialModel and indicate wether or not it is possible to make it - # @note The states ( initial_state and new_state ) contains only fields that changes - # @param model model : The EditorialModel.model object to provide the global context + ## @brief Modify the db given an EM change + # @param em model : The EditorialModel.model object to provide the global context # @param uid int : The uid of the change EmComponent # @param initial_state dict | None : dict with field name as key and field value as value. Representing the original state. None mean creation of a new component. # @param new_state dict | None : dict with field name as key and field value as value. Representing the new state. None mean component deletion # @throw EditorialModel.exceptions.MigrationHandlerChangeError if the change was refused - def register_change(self, model, uid, initial_state, new_state): - # find type of component change - if initial_state is None: - state_change = 'new' - elif new_state is None: - state_change = 'del' - else: - state_change = 'upgrade' + def register_change(self, em, uid, initial_state, new_state, engine=None): + if engine is None: + engine = self.db_engine + if isinstance(em.component(uid), EditorialModel.classes.EmClass): + if initial_state is None: + #EmClass creation + self.create_emclass_table(em, uid, engine) + elif new_state is None: + #EmClass deletion + self.delete_emclass_table(em, uid) + elif isinstance(em.component(uid), EditorialModel.fields.EmField): + emfield = em.component(uid) + if emfield.rel_field_id is None: + #non relationnal field + if initial_state is None: + #non relationnal EmField creation + if emfield.name not in EditorialModel.classtypes.common_fields.keys(): + self.add_col_from_emfield(em, uid) + elif new_state is None: + #non relationnal EmField deletion + if emfield.name not in EditorialModel.classtypes.common_fields.keys(): + self.del_col_from_emfield(em, uid) + else: + #relationnal field + if initial_state is None: + #Rel2type attr creation + self.add_relationnal_field(em, uid) + elif new_state is None: + #Rel2type attr deletion + self.del_relationnal_field(em, uid) - # call method to handle the database change - component_name = Model.name_from_emclass(type(model.component(uid))) - handler_func = component_name.lower() + '_' + state_change - if hasattr(self, handler_func): - getattr(self, handler_func)(model, uid, initial_state, new_state) + ## @brief dumdumdummy + # @note implemented to avoid the log message of EditorialModel.migrationhandler.dummy.DummyMigrationHandler + def register_model_state(self, em, state_hash): + pass - # New Class, a table must be created - def emclass_new(self, model, uid, initial_state, new_state): - class_table_name = self._class_table_name(new_state['name']) - self._query_bd( - create(table=class_table_name, column=[self._pk_column]) - ) - - # New Field, must create a column in Class table or in Class_Type relational attribute table - # @todo common fields creation does not allow to add new common fields. It should - def emfield_new(self, model, uid, initial_state, new_state): - - # field is of type rel2type, create the relational class_type table and return - if new_state['fieldtype'] == 'rel2type': - # find relational_type name, and class name of the field - class_name = self._class_table_name_from_field(model, new_state) - type_name = model.component(new_state['rel_to_type_id']).name - table_name = self._relational_table_name(class_name, type_name) - self._query_bd( - create(table=table_name, column=[self._pk_column]), - ) - return - - # Column creation - # - # field is internal, create a column in the objects table - if new_state['internal']: - if new_state['fieldtype'] == 'pk': # this column has already beeen created by self._install_tables() - return - if new_state['name'] in EditorialModel.classtypes.common_fields: # this column has already beeen created by self._install_tables() - return - - # field is relational (rel_field_id), create a column in the class_type table - elif new_state['rel_field_id']: - class_name = self._class_table_name_from_field(model, new_state) - rel_type_id = model.component(new_state['rel_field_id']).rel_to_type_id - type_name = model.component(rel_type_id).name - table_name = self._relational_table_name(class_name, type_name) - - # else create a column in the class table - else: - table_name = self._class_table_name_from_field(model, new_state) - - field_definition = self._fieldtype_definition(new_state['fieldtype'], new_state) - self._query_bd( - alter_add(table=table_name, column=[(new_state['name'],field_definition)]) - ) - - ## convert fieldtype name to SQL definition - def _fieldtype_definition(self, fieldtype, options): - basic_type = GenericFieldType.from_name(fieldtype).ftype - if basic_type == 'int': - return 'INT' - elif basic_type == 'char': - max_length = options['max_length'] if 'max_length' in options else 255 - return 'CHAR(%s)' % max_length - elif basic_type == 'text': - return 'TEXT' - elif basic_type == 'bool': - return 'BOOLEAN' - elif basic_type == 'datetime': - definition = 'DATETIME' - if 'now_on_create' in options and options['now_on_create']: - definition += ' DEFAULT CURRENT_TIMESTAMP' - #if 'now_on_update' in options and options['now_on_update']: - #definition += ' ON UPDATE CURRENT_TIMESTAMP' - return definition - - raise EditorialModel.exceptions.MigrationHandlerChangeError("Basic type '%s' of fieldtype '%s' is not compatible with SQL migration Handler" % basic_type, fieldtype) - - ## Test if internal tables must be created, create it if it must - def _install_tables(self): - # create common fields definition - common_fields = [self._pk_column] - for name, options in EditorialModel.classtypes.common_fields.items(): - if options['fieldtype'] != 'pk': - common_fields.append((name, self._fieldtype_definition(options['fieldtype'], options))) - - # create common tables - self._query_bd( - create(table=self._main_table_name, column=common_fields), - create(table=self._relation_table_name, column=[('relation_id','INTEGER PRIMARY KEY AUTOINCREMENT'), ('superior_id','INT'), ('subdordinate_id','INT'), ('nature','CHAR(255)'), ('depth','INT'), ('rank','INT')]) - ) - - def _query_bd(self, *queries): - with self.db as cur: - for query in queries: - print(query) + ## @brief Exec a query + # @param query str : SQL query + def _query(self, query): + if self.debug: + print(query + "\n") + if not self.dryrun: + with self.db.cursor() as cur: cur.execute(query) + self.db.commit() # autocommit - def _class_table_name(self, class_name): - return 'class_' + class_name + ## @brief Add a relationnal field + # Add a rel2type attribute + # @note this function handles the table creation + # @param em Model : EditorialModel.model.Model instance + # @param rfuid int : Relationnal field uid + def add_relationnal_field(self, em, rfuid): + emfield = em.component(rfuid) + if not isinstance(emfield, EditorialModel.fields.EmField): + raise ValueError("The given uid is not an EmField uid") - def _relational_table_name(self, class_name, type_name): - return 'r2t_' + class_name + '_' + type_name + r2tf = em.component(emfield.rel_field_id) + tname = self._r2t2table_name(em, r2tf) + pkname, pkftype = self._relation_pk - def _class_table_name_from_field(self, model, field): - class_id = field['class_id'] - class_name = model.component(class_id).name - class_table_name = self._class_table_name(class_name) - return class_table_name + #If not exists create a relational table + self._create_table(tname, pkname, pkftype, self.db_engine, if_exists='nothing') + #Add a foreign key if wanted + if self.foreign_keys: + self._add_fk(tname, self.datasource.relations_table_name, pkname, pkname) + #Add the column + self._add_column(tname, emfield.name, emfield.fieldtype_instance()) + #Update table triggers + self._generate_triggers(tname, self._r2type2cols(em, r2tf)) + + ## @brief Delete a rel2type attribute + # + # Delete a rel2type attribute + # @note this method handles the table deletion + # @param em Model : EditorialModel.model.Model instance + # @param rfuid int : Relationnal field uid + def del_relationnal_field(self, em, rfuid): + emfield = em.component(rfuid) + if not isinstance(emfield, EditorialModel.fields.EmField): + raise ValueError("The given uid is not an EmField uid") + + r2tf = em.component(emfield.rel_field_id) + tname = self._r2t2table_name(em, r2tf) + + if len(self._r2type2cols(em, r2tf)) == 1: + #The table can be deleted (no more attribute for this rel2type) + self._query("""DROP TABLE {table_name}""".format(table_name=tname)) + else: + self._del_column(tname, emfield.name) + #Update table triggers + self._generate_triggers(tname, self._r2type2cols(em, r2tf)) + + ## @brief Given an EmField uid add a column to the corresponding table + # @param em Model : A Model instance + # @param uid int : An EmField uid + def add_col_from_emfield(self, em, uid): + emfield = em.component(uid) + if not isinstance(emfield, EditorialModel.fields.EmField): + raise ValueError("The given uid is not an EmField uid") + + emclass = emfield.em_class + tname = self._emclass2table_name(emclass) + self._add_column(tname, emfield.name, emfield.fieldtype_instance()) + # Refresh the table triggers + cols_l = self._class2cols(emclass) + self._generate_triggers(tname, cols_l) + + ## @brief Given a class uid create the coressponding table + # @param em Model : A Model instance + # @param uid int : An EmField uid + def create_emclass_table(self, em, uid, engine): + emclass = em.component(uid) + if not isinstance(emclass, EditorialModel.classes.EmClass): + raise ValueError("The given uid is not an EmClass uid") + pkname, pktype = self._common_field_pk + table_name = self._emclass2table_name(emclass) + self._create_table(table_name, pkname, pktype, engine=engine) + + if self.foreign_keys: + self._add_fk(table_name, self.datasource.objects_table_name, pkname, pkname) + + ## @brief Given an EmClass uid delete the corresponding table + # @param em Model : A Model instance + # @param uid int : An EmField uid + def delete_emclass_table(self, em, uid): + emclass = em.component(uid) + if not isinstance(emclass, EditorialModel.classes.EmClass): + raise ValueError("The give uid is not an EmClass uid") + tname = self._emclass2table_name(emclass) + # Delete the table triggers to prevent errors + self._generate_triggers(tname, dict()) + + tname = self.datasource.escape_idname(tname) + + self._query("""DROP TABLE {table_name};""".format(table_name=tname)) + + ## @brief Given an EmField delete the corresponding column + # @param em Model : an @ref EditorialModel.model.Model instance + # @param uid int : an EmField uid + def delete_col_from_emfield(self, em, uid): + emfield = em.component(uid) + if not isinstance(emfield, EditorialModel.fields.EmField): + raise ValueError("The given uid is not an EmField uid") + + emclass = emfield.em_class + tname = self._emclass2table_name(emclass) + # Delete the table triggers to prevent errors + self._generate_triggers(tname, dict()) + + self._del_column(tname, emfield.name) + # Refresh the table triggers + cols_ls = self._class2cols(emclass) + self._generate_triggers(tname, cols_l) + + ## @brief Delete a column from a table + # @param tname str : The table name + # @param fname str : The column name + def _del_column(self, tname, fname): + tname = self.datasource.escape_idname(tname) + fname = self.datasource.escape_idname(fname) + + self._query("""ALTER TABLE {table_name} DROP COLUMN {col_name};""".format(table_name=tname, col_name=fname)) + + ## @brief Construct a table name given an EmClass instance + # @param emclass EmClass : An EmClass instance + # @return a table name + def _emclass2table_name(self, emclass): + return self.datasource.get_table_name_from_class(emclass.name) + #return "class_%s"%emclass.name + + ## @brief Construct a table name given a rela2type EmField instance + # @param em Model : A Model instance + # @param emfield EmField : An EmField instance + # @return a table name + def _r2t2table_name(self, em, emfield): + emclass = emfield.em_class + emtype = em.component(emfield.rel_to_type_id) + return self.datasource.get_r2t2table_name(emclass.name, emtype.name) + #return "%s_%s_%s"%(emclass.name, emtype.name, emfield.name) + + ## @brief Generate a columns_fieldtype dict given a rel2type EmField + # @param em Model : an @ref EditorialModel.model.Model instance + # @param emfield EmField : and @ref EditorialModel.fields.EmField instance + def _r2type2cols(self, em, emfield): + return {f.name: f.fieldtype_instance() for f in em.components('EmField') if f.rel_field_id == emfield.uid} + + ## @brief Generate a columns_fieldtype dict given an EmClass + # @param emclass EmClass : An EmClass instance + # @return A dict with column name as key and EmFieldType instance as value + def _class2cols(self, emclass): + if not isinstance(emclass, EditorialModel.classes.EmClass): + raise ValueError("The given uid is not an EmClass uid") + return {f.name: f.fieldtype_instance() for f in emclass.fields() if f.name not in EditorialModel.classtypes.common_fields.keys()} + + ## @brief Create object and relations tables + # @param drop_if_exist bool : If true drop tables if exists + def _create_default_tables(self, drop_if_exist=False): + if_exists = 'drop' if drop_if_exist else 'nothing' + #Object tablea + tname = self.datasource.objects_table_name + pk_name, pk_ftype = self._common_field_pk + self._create_table(tname, pk_name, pk_ftype, engine=self.db_engine, if_exists=if_exists) + #Adding columns + cols = {fname: self._common_field_to_ftype(fname) for fname in EditorialModel.classtypes.common_fields} + for fname, ftype in cols.items(): + if fname != pk_name: + self._add_column(tname, fname, ftype) + #Creating triggers + self._generate_triggers(tname, cols) + + #Relation table + tname = self.datasource.relations_table_name + pk_name, pk_ftype = self._relation_pk + self._create_table(tname, pk_name, pk_ftype, engine=self.db_engine, if_exists=if_exists) + #Adding columns + for fname, ftype in self._relation_cols.items(): + self._add_column(tname, fname, ftype) + #Creating triggers + self._generate_triggers(tname, self._relation_cols) + + ## @return true if the name changes + def _name_change(self, initial_state, new_state): + return 'name' in initial_state and initial_state['name'] != new_state['name'] + + ## @brief Create a table with primary key + # @param table_name str : table name + # @param pk_name str : pk column name + # @param pk_specs str : see @ref _field_to_sql() + # @param engine str : The engine to use with this table + # @param charset str : The charset of this table + # @param if_exist str : takes values in ['nothing', 'drop'] + def _create_table(self, table_name, pk_name, pk_ftype, engine, charset='utf8', if_exists='nothing'): + #Escaped table name + etname = self.datasource.escape_idname(table_name) + pk_type = self._field_to_type(pk_ftype) + pk_specs = self._field_to_specs(pk_ftype) + + if if_exists == 'drop': + self._query("""DROP TABLE IF EXISTS {table_name};""".format(table_name=etname)) + qres = """ +CREATE TABLE {table_name} ( +{pk_name} {pk_type} {pk_specs}, +PRIMARY KEY({pk_name}) +) ENGINE={engine} DEFAULT CHARSET={charset};""" + elif if_exists == 'nothing': + qres = """CREATE TABLE IF NOT EXISTS {table_name} ( +{pk_name} {pk_type} {pk_specs}, +PRIMARY KEY({pk_name}) +) ENGINE={engine} DEFAULT CHARSET={charset};""" + else: + raise ValueError("Unexpected value for argument if_exists '%s'." % if_exists) + + self._query(qres.format( + table_name=self.datasource.escape_idname(table_name), + pk_name=self.datasource.escape_idname(pk_name), + pk_type=pk_type, + pk_specs=pk_specs, + engine=engine, + charset=charset + )) + + ## @brief Add a column to a table + # @param table_name str : The table name + # @param col_name str : The columns name + # @param col_fieldtype EmFieldype the fieldtype + def _add_column(self, table_name, col_name, col_fieldtype, drop_if_exists=False): + add_col = """ALTER TABLE {table_name} +ADD COLUMN {col_name} {col_type} {col_specs};""" + + etname = self.datasource.escape_idname(table_name) + ecname = self.datasource.escape_idname(col_name) + + add_col = add_col.format( + table_name=etname, + col_name=ecname, + col_type=self._field_to_type(col_fieldtype), + col_specs=self._field_to_specs(col_fieldtype), + ) + try: + self._query(add_col) + except self._dbmodule.err.InternalError as e: + if drop_if_exists: + self._del_column(table_name, col_name) + self._add_column(table_name, col_name, col_fieldtype, drop_if_exists) + else: + #LOG + print("Aborded, column `%s` exists" % col_name) + + ## @brief Add a foreign key + # @param src_table_name str : The name of the table where we will add the FK + # @param dst_table_name str : The name of the table the FK will point on + # @param src_col_name str : The name of the concerned column in the src_table + # @param dst_col_name str : The name of the concerned column in the dst_table + def _add_fk(self, src_table_name, dst_table_name, src_col_name, dst_col_name): + stname = self.datasource.escape_idname(src_table_name) + dtname = self.datasource.escape_idname(dst_table_name) + scname = self.datasource.escape_idname(src_col_name) + dcname = self.datasource.escape_idname(dst_col_name) + + fk_name = self.datasource.get_fk_name(src_table_name, dst_table_name) + + self._del_fk(src_table_name, dst_table_name) + + self._query("""ALTER TABLE {src_table} +ADD CONSTRAINT {fk_name} +FOREIGN KEY ({src_col}) references {dst_table}({dst_col});""".format( + fk_name=self.datasource.escape_idname(fk_name), + src_table=stname, + src_col=scname, + dst_table=dtname, + dst_col=dcname + )) + + ## @brief Given a source and a destination table, delete the corresponding FK + # @param src_table_name str : The name of the table where the FK is + # @param dst_table_name str : The name of the table the FK point on + # @warning fails silently + def _del_fk(self, src_table_name, dst_table_name): + try: + self._query("""ALTER TABLE {src_table} +DROP FOREIGN KEY {fk_name}""".format( + src_table=self.datasource.escape_idname(src_table_name), + fk_name=self.datasource.escape_idname(self.datasource.get_fk_name(src_table_name, dst_table_name)) + )) + except self._dbmodule.err.InternalError: + # If the FK don't exists we do not care + pass + + ## @brief Generate triggers given a table_name and its columns fieldtypes + # @param table_name str : Table name + # @param cols_ftype dict : with col name as key and column fieldtype as value + def _generate_triggers(self, table_name, cols_ftype): + colval_l_upd = dict() # param for update trigger + colval_l_ins = dict() # param for insert trigger + + for cname, cftype in cols_ftype.items(): + if cftype.ftype == 'datetime': + if cftype.now_on_update: + colval_l_upd[cname] = 'NOW()' + if cftype.now_on_create: + colval_l_ins[cname] = 'NOW()' + + self._table_trigger(table_name, 'UPDATE', colval_l_upd) + self._table_trigger(table_name, 'INSERT', colval_l_ins) + + ## @brief Create trigger for a table + # + # Primarly designed to create trigger for DATETIME types + # The method generates triggers of the form + # + # CREATE TRIGGER BEFORE ON + # FOR EACH ROW SET + # NEW. = , + # ; + # @param table_name str : The table name + # @param moment str : can be 'update' or 'insert' + # @param cols_val dict : Dict with column name as key and column value as value + def _table_trigger(self, table_name, moment, cols_val): + trigger_name = self.datasource.escape_idname("%s_%s_trig" % (table_name, moment)) + #Try to delete the trigger + drop_trig = """DROP TRIGGER IF EXISTS {trigger_name};""".format(trigger_name=trigger_name) + self._query(drop_trig) + + col_val_l = ', '.join(["NEW.%s = %s" % (self.datasource.escape_idname(cname), cval)for cname, cval in cols_val.items()]) + #Create a trigger if needed + if len(col_val_l) > 0: + trig_q = """CREATE TRIGGER {trigger_name} BEFORE {moment} ON {table_name} +FOR EACH ROW SET {col_val_list};""".format( + trigger_name=trigger_name, + table_name=self.datasource.escape_idname(table_name), + moment=moment, col_val_list=col_val_l + ) + self._query(trig_q) + + ## @brief Identifier escaping + # @param idname str : An SQL identifier + #def _idname_escape(self, idname): + # if '`' in idname: + # raise ValueError("Invalid name : '%s'"%idname) + # return '`%s`'%idname + + ## @brief Returns column specs from fieldtype + # @param emfieldtype EmFieldType : An EmFieldType insance + # @todo escape default value + def _field_to_specs(self, emfieldtype): + colspec = '' + if not emfieldtype.nullable: + colspec = 'NOT NULL' + if hasattr(emfieldtype, 'default'): + colspec += ' DEFAULT ' + if emfieldtype.default is None: + colspec += 'NULL ' + else: + colspec += emfieldtype.default # ESCAPE VALUE HERE !!!! + + if emfieldtype.name == 'pk': + colspec += ' AUTO_INCREMENT' + + return colspec + + ## @brief Given a fieldtype return a MySQL type specifier + # @param emfieldtype EmFieldType : A fieldtype + # @return the corresponding MySQL type + def _field_to_type(self, emfieldtype): + ftype = emfieldtype.ftype + + if ftype == 'char' or ftype == 'str': + res = "VARCHAR(%d)" % emfieldtype.max_length + elif ftype == 'text': + res = "TEXT" + elif ftype == 'datetime': + res = "DATETIME" + # client side workaround for only one column with CURRENT_TIMESTAMP : giving NULL to timestamp that don't allows NULL + # cf. https://dev.mysql.com/doc/refman/5.0/en/timestamp-initialization.html#idm139961275230400 + # The solution for the migration handler is to create triggers : + # CREATE TRIGGER trigger_name BEFORE INSERT ON `my_super_table` + # FOR EACH ROW SET NEW.my_date_column = NOW(); + # and + # CREATE TRIGGER trigger_name BEFORE UPDATE ON + + elif ftype == 'bool': + res = "BOOL" + elif ftype == 'int': + res = "INT" + elif ftype == 'rel2type': + res = "INT" + else: + raise ValueError("Unsuported fieldtype ftype : %s" % ftype) + + return res + + ## @brief Returns a tuple (pkname, pk_ftype) + @property + def _common_field_pk(self): + for fname, fta in EditorialModel.classtypes.common_fields.items(): + if fta['fieldtype'] == 'pk': + return (fname, self._common_field_to_ftype(fname)) + return (None, None) + + ## @brief Returns a tuple (rel_pkname, rel_ftype) + # @todo do it + @property + def _relation_pk(self): + return (MySQL.relations_pkname, EditorialModel.fieldtypes.pk.EmFieldType()) + + ## @brief Returns a dict { colname:fieldtype } of relation table columns + @property + def _relation_cols(self): + from_name = EditorialModel.fieldtypes.generic.GenericFieldType.from_name + return { + 'id_sup': from_name('integer')(), + 'id_sub': from_name('integer')(), + 'rank': from_name('integer')(nullable=True), + 'depth': from_name('integer')(nullable=True), + 'nature': from_name('char')(max_lenght=10, nullable=True), + } + + ## @brief Given a common field name return an EmFieldType instance + # @param cname str : Common field name + # @return An EmFieldType instance + def _common_field_to_ftype(self, cname): + fta = copy.copy(EditorialModel.classtypes.common_fields[cname]) + fto = EditorialModel.fieldtypes.generic.GenericFieldType.from_name(fta['fieldtype']) + del fta['fieldtype'] + return fto(**fta) diff --git a/DataSource/MySQL/migrationhandler__future__.py b/DataSource/MySQL/migrationhandler__future__.py new file mode 100644 index 0000000..e459cec --- /dev/null +++ b/DataSource/MySQL/migrationhandler__future__.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- + +## @package EditorialModel.migrationhandler.sql +# @brief A dummy migration handler +# +# According to it every modifications are possible +# + +import EditorialModel +from DataSource.dummy.migrationhandler import DummyMigrationHandler +from EditorialModel.fieldtypes.generic import GenericFieldType +from EditorialModel.model import Model +from mosql.db import Database +from Lodel.utils.mosql import create, alter_add + + +## Manage Model changes +class SQLMigrationHandler(DummyMigrationHandler): + + fieldtype_to_sql = { + 'char': "CHAR(255)", + 'integer': 'INT' + } + + def __init__(self, module=None, *conn_args, **conn_kargs): + super(SQLMigrationHandler, self).__init__(False) + + self.db = Database(module, *conn_args, **conn_kargs) + self._pk_column = (EditorialModel.classtypes.pk_name(), 'INTEGER PRIMARY KEY AUTO_INCREMENT') + self._main_table_name = 'object' + self._relation_table_name = 'relation' + + self._install_tables() + + ## @brief Record a change in the EditorialModel and indicate wether or not it is possible to make it + # @note The states ( initial_state and new_state ) contains only fields that changes + # @param model model : The EditorialModel.model object to provide the global context + # @param uid int : The uid of the change EmComponent + # @param initial_state dict | None : dict with field name as key and field value as value. Representing the original state. None mean creation of a new component. + # @param new_state dict | None : dict with field name as key and field value as value. Representing the new state. None mean component deletion + # @throw EditorialModel.exceptions.MigrationHandlerChangeError if the change was refused + def register_change(self, model, uid, initial_state, new_state): + # find type of component change + if initial_state is None: + state_change = 'new' + elif new_state is None: + state_change = 'del' + else: + state_change = 'upgrade' + + # call method to handle the database change + component_name = Model.name_from_emclass(type(model.component(uid))) + handler_func = component_name.lower() + '_' + state_change + if hasattr(self, handler_func): + getattr(self, handler_func)(model, uid, initial_state, new_state) + + # New Class, a table must be created + def emclass_new(self, model, uid, initial_state, new_state): + class_table_name = self._class_table_name(new_state['name']) + self._query_bd( + create(table=class_table_name, column=[self._pk_column]) + ) + + # New Field, must create a column in Class table or in Class_Type relational attribute table + # @todo common fields creation does not allow to add new common fields. It should + def emfield_new(self, model, uid, initial_state, new_state): + + # field is of type rel2type, create the relational class_type table and return + if new_state['fieldtype'] == 'rel2type': + # find relational_type name, and class name of the field + class_name = self._class_table_name_from_field(model, new_state) + type_name = model.component(new_state['rel_to_type_id']).name + table_name = self._relational_table_name(class_name, type_name) + self._query_bd( + create(table=table_name, column=[self._pk_column]), + ) + return + + # Column creation + # + # field is internal, create a column in the objects table + if new_state['internal']: + if new_state['fieldtype'] == 'pk': # this column has already beeen created by self._install_tables() + return + if new_state['name'] in EditorialModel.classtypes.common_fields: # this column has already beeen created by self._install_tables() + return + + # field is relational (rel_field_id), create a column in the class_type table + elif new_state['rel_field_id']: + class_name = self._class_table_name_from_field(model, new_state) + rel_type_id = model.component(new_state['rel_field_id']).rel_to_type_id + type_name = model.component(rel_type_id).name + table_name = self._relational_table_name(class_name, type_name) + + # else create a column in the class table + else: + table_name = self._class_table_name_from_field(model, new_state) + + field_definition = self._fieldtype_definition(new_state['fieldtype'], new_state) + self._query_bd( + alter_add(table=table_name, column=[(new_state['name'],field_definition)]) + ) + + ## convert fieldtype name to SQL definition + def _fieldtype_definition(self, fieldtype, options): + basic_type = GenericFieldType.from_name(fieldtype).ftype + if basic_type == 'int': + return 'INT' + elif basic_type == 'char': + max_length = options['max_length'] if 'max_length' in options else 255 + return 'CHAR(%s)' % max_length + elif basic_type == 'text': + return 'TEXT' + elif basic_type == 'bool': + return 'BOOLEAN' + elif basic_type == 'datetime': + definition = 'DATETIME' + if 'now_on_create' in options and options['now_on_create']: + definition += ' DEFAULT CURRENT_TIMESTAMP' + #if 'now_on_update' in options and options['now_on_update']: + #definition += ' ON UPDATE CURRENT_TIMESTAMP' + return definition + + raise EditorialModel.exceptions.MigrationHandlerChangeError("Basic type '%s' of fieldtype '%s' is not compatible with SQL migration Handler" % basic_type, fieldtype) + + ## Test if internal tables must be created, create it if it must + def _install_tables(self): + # create common fields definition + common_fields = [self._pk_column] + for name, options in EditorialModel.classtypes.common_fields.items(): + if options['fieldtype'] != 'pk': + common_fields.append((name, self._fieldtype_definition(options['fieldtype'], options))) + + # create common tables + self._query_bd( + create(table=self._main_table_name, column=common_fields), + create(table=self._relation_table_name, column=[('relation_id','INTEGER PRIMARY KEY AUTOINCREMENT'), ('superior_id','INT'), ('subdordinate_id','INT'), ('nature','CHAR(255)'), ('depth','INT'), ('rank','INT')]) + ) + + def _query_bd(self, *queries): + with self.db as cur: + for query in queries: + print(query) + cur.execute(query) + + def _class_table_name(self, class_name): + return 'class_' + class_name + + def _relational_table_name(self, class_name, type_name): + return 'r2t_' + class_name + '_' + type_name + + def _class_table_name_from_field(self, model, field): + class_id = field['class_id'] + class_name = model.component(class_id).name + class_table_name = self._class_table_name(class_name) + return class_table_name diff --git a/README.md b/README.md index 27b9275..16c7f29 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ +Creating a Lodel "instance": + +use the lodel_init.sh script : + lodel_init.sh INSTANCE_NAME INSTANCE_WANTED_PATH [LODEL2_LIB_PATH] + +Once the instance is created you can run an interactive python interpreter using : + cd INSTANCE_PATH; python loader.py + +If you want to write a script that run is the instance env you have to from loader import * + First test installation : - use python 3.4 diff --git a/install/Makefile b/install/Makefile new file mode 100644 index 0000000..bcf4cce --- /dev/null +++ b/install/Makefile @@ -0,0 +1,17 @@ +all: + +refreshdyn: + python -c "import utils; utils.refreshdyn()" + +dbinit: + python -c "import utils; utils.db_init()" + +.PHONY: clean cleanpycache cleanpyc refreshdyn + +clean: cleanpycache + +cleanpyc: + -@rm -v *.pyc + +cleanpycache: cleanpyc + -@rm -vR __pycache__ diff --git a/install/dynleapi.py b/install/dynleapi.py new file mode 100644 index 0000000..03d2bd0 --- /dev/null +++ b/install/dynleapi.py @@ -0,0 +1,171 @@ +## @author LeFactory + +import EditorialModel +from EditorialModel import fieldtypes +from EditorialModel.fieldtypes import naturerelation, char, integer, datetime, pk + +import leapi +import leapi.lecrud +import leapi.leobject +import leapi.lerelation +from leapi.leclass import _LeClass +from leapi.letype import _LeType + +import DataSource.MySQL.leapidatasource + + +## @brief _LeCrud concret class +# @see leapi.lecrud._LeCrud +class LeCrud(leapi.lecrud._LeCrud): + _datasource = DataSource.MySQL.leapidatasource.LeDataSourceSQL(**{}) + _uid_fieldtype = None + +## @brief _LeObject concret class +# @see leapi.leobject._LeObject +class LeObject(LeCrud, leapi.leobject._LeObject): + _me_uid = {1: 'Textes', 2: 'Personnes', 19: 'Numero', 5: 'Article', 6: 'Personne', 13: 'Publication', 14: 'Rubrique'} + _uid_fieldtype = { 'lodel_id': EditorialModel.fieldtypes.pk.EmFieldType(**{'internal': 'automatic'}) } + _leo_fieldtypes = { + 'creation_date': EditorialModel.fieldtypes.datetime.EmFieldType(**{'now_on_create': True, 'internal': 'automatic'}), + 'string': EditorialModel.fieldtypes.char.EmFieldType(**{'max_length': 128, 'internal': 'automatic'}), + 'modification_date': EditorialModel.fieldtypes.datetime.EmFieldType(**{'now_on_update': True, 'now_on_create': True, 'internal': 'automatic'}), + 'type_id': EditorialModel.fieldtypes.integer.EmFieldType(**{'internal': 'automatic'}), + 'class_id': EditorialModel.fieldtypes.integer.EmFieldType(**{'internal': 'automatic'}) + } + +## @brief _LeRelation concret class +# @see leapi.lerelation._LeRelation +class LeRelation(LeCrud, leapi.lerelation._LeRelation): + _uid_fieldtype = { 'id_relation': EditorialModel.fieldtypes.pk.EmFieldType(**{'internal': 'automatic'}) } + _rel_fieldtypes = { + 'rank': EditorialModel.fieldtypes.integer.EmFieldType(**{'internal': 'automatic'}), + 'nature': EditorialModel.fieldtypes.naturerelation.EmFieldType(**{}), + 'depth': EditorialModel.fieldtypes.integer.EmFieldType(**{'internal': 'automatic'}) + } + _rel_attr_fieldtypes = dict() + +class LeHierarch(LeRelation, leapi.lerelation._LeHierarch): + _rel_attr_fieldtypes = dict() + +class LeRel2Type(LeRelation, leapi.lerelation._LeRel2Type): + pass + +class LeClass(LeObject, _LeClass): + pass + +class LeType(LeClass, _LeType): + pass + +## @brief EmClass Textes LeClass child class +# @see leapi.leclass.LeClass +class Textes(LeClass, LeObject): + _class_id = 1 + + +## @brief EmClass Personnes LeClass child class +# @see leapi.leclass.LeClass +class Personnes(LeClass, LeObject): + _class_id = 2 + + +## @brief EmClass Publication LeClass child class +# @see leapi.leclass.LeClass +class Publication(LeClass, LeObject): + _class_id = 13 + + +## @brief EmType Article LeType child class +# @see leobject::letype::LeType +class Article(LeType, Textes): + _type_id = 5 + + +## @brief EmType Personne LeType child class +# @see leobject::letype::LeType +class Personne(LeType, Personnes): + _type_id = 6 + + +## @brief EmType Rubrique LeType child class +# @see leobject::letype::LeType +class Rubrique(LeType, Publication): + _type_id = 14 + + +## @brief EmType Numero LeType child class +# @see leobject::letype::LeType +class Numero(LeType, Publication): + _type_id = 19 + + +class Rel_textes2personne(LeRel2Type): + _rel_attr_fieldtypes = { + 'adresse': EditorialModel.fieldtypes.char.EmFieldType(**{'uniq': False, 'nullable': True, 'internal': False}) +} + + +#Initialisation of Textes class attributes +Textes._fieldtypes = { + 'modification_date': EditorialModel.fieldtypes.datetime.EmFieldType(**{'nullable': False, 'uniq': False, 'now_on_update': True, 'now_on_create': True, 'internal': 'automatic'}), + 'soustitre': EditorialModel.fieldtypes.char.EmFieldType(**{'uniq': False, 'nullable': True, 'internal': False}), + 'string': EditorialModel.fieldtypes.char.EmFieldType(**{'nullable': True, 'uniq': False, 'max_length': 128, 'internal': 'automatic'}), + 'titre': EditorialModel.fieldtypes.char.EmFieldType(**{'uniq': False, 'nullable': True, 'internal': False}), + 'bleu': EditorialModel.fieldtypes.char.EmFieldType(**{'uniq': False, 'nullable': True, 'internal': False}), + 'lodel_id': EditorialModel.fieldtypes.pk.EmFieldType(**{'uniq': False, 'nullable': False, 'internal': 'automatic'}), + 'type_id': EditorialModel.fieldtypes.integer.EmFieldType(**{'uniq': False, 'nullable': False, 'internal': 'automatic'}), + 'class_id': EditorialModel.fieldtypes.integer.EmFieldType(**{'uniq': False, 'nullable': False, 'internal': 'automatic'}), + 'creation_date': EditorialModel.fieldtypes.datetime.EmFieldType(**{'uniq': False, 'nullable': False, 'now_on_create': True, 'internal': 'automatic'}) +} +Textes._linked_types = [Personne] +Textes._classtype = 'entity' + +#Initialisation of Personnes class attributes +Personnes._fieldtypes = { + 'nom': EditorialModel.fieldtypes.char.EmFieldType(**{'uniq': False, 'nullable': True, 'internal': False}), + 'age': EditorialModel.fieldtypes.char.EmFieldType(**{'uniq': False, 'nullable': True, 'internal': False}), + 'prenom': EditorialModel.fieldtypes.char.EmFieldType(**{'uniq': False, 'nullable': True, 'internal': False}), + 'string': EditorialModel.fieldtypes.char.EmFieldType(**{'nullable': True, 'uniq': False, 'max_length': 128, 'internal': 'automatic'}), + 'lodel_id': EditorialModel.fieldtypes.pk.EmFieldType(**{'uniq': False, 'nullable': False, 'internal': 'automatic'}), + 'modification_date': EditorialModel.fieldtypes.datetime.EmFieldType(**{'nullable': False, 'uniq': False, 'now_on_update': True, 'now_on_create': True, 'internal': 'automatic'}), + 'type_id': EditorialModel.fieldtypes.integer.EmFieldType(**{'uniq': False, 'nullable': False, 'internal': 'automatic'}), + 'class_id': EditorialModel.fieldtypes.integer.EmFieldType(**{'uniq': False, 'nullable': False, 'internal': 'automatic'}), + 'creation_date': EditorialModel.fieldtypes.datetime.EmFieldType(**{'uniq': False, 'nullable': False, 'now_on_create': True, 'internal': 'automatic'}) +} +Personnes._linked_types = [] +Personnes._classtype = 'person' + +#Initialisation of Publication class attributes +Publication._fieldtypes = { + 'modification_date': EditorialModel.fieldtypes.datetime.EmFieldType(**{'nullable': False, 'uniq': False, 'now_on_update': True, 'now_on_create': True, 'internal': 'automatic'}), + 'creation_date': EditorialModel.fieldtypes.datetime.EmFieldType(**{'uniq': False, 'nullable': False, 'now_on_create': True, 'internal': 'automatic'}), + 'string': EditorialModel.fieldtypes.char.EmFieldType(**{'nullable': True, 'uniq': False, 'max_length': 128, 'internal': 'automatic'}), + 'lodel_id': EditorialModel.fieldtypes.pk.EmFieldType(**{'uniq': False, 'nullable': False, 'internal': 'automatic'}), + 'titre': EditorialModel.fieldtypes.char.EmFieldType(**{'uniq': False, 'nullable': True, 'internal': False}), + 'type_id': EditorialModel.fieldtypes.integer.EmFieldType(**{'uniq': False, 'nullable': False, 'internal': 'automatic'}), + 'class_id': EditorialModel.fieldtypes.integer.EmFieldType(**{'uniq': False, 'nullable': False, 'internal': 'automatic'}) +} +Publication._linked_types = [] +Publication._classtype = 'entity' + +#Initialisation of Article class attributes +Article._fields = ['titre', 'class_id', 'soustitre', 'string', 'type_id', 'lodel_id', 'modification_date', 'creation_date'] +Article._superiors = {'parent': [Rubrique]} +Article._leclass = Textes + +#Initialisation of Personne class attributes +Personne._fields = ['nom', 'class_id', 'prenom', 'string', 'type_id', 'lodel_id', 'modification_date', 'creation_date'] +Personne._superiors = {} +Personne._leclass = Personnes + +#Initialisation of Rubrique class attributes +Rubrique._fields = ['titre', 'class_id', 'string', 'type_id', 'lodel_id', 'modification_date', 'creation_date'] +Rubrique._superiors = {'parent': [Rubrique, Numero]} +Rubrique._leclass = Publication + +#Initialisation of Numero class attributes +Numero._fields = ['titre', 'class_id', 'string', 'type_id', 'lodel_id', 'modification_date', 'creation_date'] +Numero._superiors = {} +Numero._leclass = Publication + +## @brief Dict for getting LeClass and LeType child classes given an EM uid +LeObject._me_uid = {1: Textes, 2: Personnes, 19: Numero, 5: Article, 6: Personne, 13: Publication, 14: Rubrique} diff --git a/install/em.json b/install/em.json new file mode 100644 index 0000000..2108112 --- /dev/null +++ b/install/em.json @@ -0,0 +1,579 @@ +{ + "1": { + "component": "EmClass", + "date_create": "Fri Oct 16 11:05:04 2015", + "date_update": "Fri Oct 16 11:05:04 2015", + "sortcolumn": "rank", + "classtype": "entity", + "icon": "0", + "rank": 1, + "name": "textes", + "help_text": "", + "string": "{\"fre\": \"Texte\"}" + }, + "2": { + "component": "EmClass", + "date_create": "Fri Oct 16 11:05:04 2015", + "date_update": "Fri Oct 16 11:05:04 2015", + "sortcolumn": "rank", + "classtype": "person", + "icon": "0", + "rank": 1, + "name": "personnes", + "help_text": "", + "string": "{\"fre\": \"Personnes\"}" + }, + "4": { + "nullable": true, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": false, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "{\"fre\": \"Titre\"}", + "class_id": 1, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 1, + "fieldtype": "char", + "help_text": "", + "internal": false, + "rel_field_id": null, + "name": "titre" + }, + "5": { + "date_update": "Fri Oct 16 11:05:04 2015", + "class_id": 1, + "icon": "0", + "name": "article", + "superiors_list": { + "parent": [ + 14 + ] + }, + "fields_list": [ + 7 + ], + "string": "{\"fre\": \"Article\"}", + "date_create": "Fri Oct 16 11:05:04 2015", + "component": "EmType", + "rank": 1, + "help_text": "", + "sortcolumn": "rank" + }, + "6": { + "date_update": "Fri Oct 16 11:05:04 2015", + "class_id": 2, + "icon": "0", + "name": "personne", + "superiors_list": {}, + "fields_list": [ + 10 + ], + "string": "{\"fre\": \"Personne\"}", + "date_create": "Fri Oct 16 11:05:04 2015", + "component": "EmType", + "rank": 1, + "help_text": "", + "sortcolumn": "rank" + }, + "7": { + "nullable": true, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": true, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "{\"fre\": \"Sous-titre\"}", + "class_id": 1, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 2, + "fieldtype": "char", + "help_text": "", + "internal": false, + "rel_field_id": null, + "name": "soustitre" + }, + "9": { + "nullable": true, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": false, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "{\"fre\": \"Nom\"}", + "class_id": 2, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 1, + "fieldtype": "char", + "help_text": "", + "internal": false, + "rel_field_id": null, + "name": "nom" + }, + "10": { + "nullable": true, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": true, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "{\"fre\": \"Pr\\u00e9nom\"}", + "class_id": 2, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 2, + "fieldtype": "char", + "help_text": "", + "internal": false, + "rel_field_id": null, + "name": "prenom" + }, + "11": { + "nullable": true, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": false, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "{\"fre\": \"Auteur\"}", + "class_id": 1, + "date_create": "Fri Oct 16 11:05:04 2015", + "rel_to_type_id": 6, + "rank": 1, + "fieldtype": "rel2type", + "help_text": "", + "internal": false, + "rel_field_id": null, + "name": "auteur" + }, + "12": { + "nullable": true, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": false, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "{\"fre\": \"Adresse\"}", + "class_id": 1, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 2, + "fieldtype": "char", + "help_text": "", + "internal": false, + "rel_field_id": 11, + "name": "adresse" + }, + "13": { + "component": "EmClass", + "date_create": "Fri Oct 16 11:05:04 2015", + "date_update": "Fri Oct 16 11:05:04 2015", + "sortcolumn": "rank", + "classtype": "entity", + "icon": "0", + "rank": 2, + "name": "publication", + "help_text": "", + "string": "{\"fre\": \"Publication\"}" + }, + "14": { + "date_update": "Fri Oct 16 11:05:04 2015", + "class_id": 13, + "icon": "0", + "name": "rubrique", + "superiors_list": { + "parent": [ + 14, + 19 + ] + }, + "fields_list": [], + "string": "{\"fre\": \"Rubrique\"}", + "date_create": "Fri Oct 16 11:05:04 2015", + "component": "EmType", + "rank": 1, + "help_text": "", + "sortcolumn": "rank" + }, + "16": { + "nullable": true, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": false, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "{\"fre\": \"Titre\"}", + "class_id": 13, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 1, + "fieldtype": "char", + "help_text": "", + "internal": false, + "rel_field_id": null, + "name": "titre" + }, + "18": { + "nullable": true, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": true, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "{\"fre\": \"Age\"}", + "class_id": 2, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 3, + "fieldtype": "char", + "help_text": "", + "internal": false, + "rel_field_id": null, + "name": "age" + }, + "19": { + "date_update": "Fri Oct 16 11:05:04 2015", + "class_id": 13, + "icon": "0", + "name": "numero", + "superiors_list": {}, + "fields_list": [], + "string": "{\"fre\": \"Num\\u00e9ro\"}", + "date_create": "Fri Oct 16 11:05:04 2015", + "component": "EmType", + "rank": 2, + "help_text": "", + "sortcolumn": "rank" + }, + "21": { + "nullable": true, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": true, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "{\"fre\": \"Bleu\"}", + "class_id": 1, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 1, + "fieldtype": "char", + "help_text": "", + "internal": false, + "rel_field_id": null, + "name": "bleu" + }, + "23": { + "nullable": false, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": false, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "", + "class_id": 1, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 1, + "fieldtype": "integer", + "help_text": "", + "internal": "automatic", + "rel_field_id": null, + "name": "class_id" + }, + "24": { + "nullable": true, + "date_update": "Fri Oct 16 11:05:04 2015", + "max_length": 128, + "icon": "0", + "optional": false, + "component": "EmField", + "string": "", + "class_id": 1, + "date_create": "Fri Oct 16 11:05:04 2015", + "name": "string", + "rank": 2, + "fieldtype": "char", + "help_text": "", + "internal": "automatic", + "rel_field_id": null, + "uniq": false + }, + "25": { + "nullable": false, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": false, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "", + "class_id": 1, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 3, + "fieldtype": "integer", + "help_text": "", + "internal": "automatic", + "rel_field_id": null, + "name": "type_id" + }, + "26": { + "nullable": false, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": false, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "", + "class_id": 1, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 4, + "fieldtype": "pk", + "help_text": "", + "internal": "automatic", + "rel_field_id": null, + "name": "lodel_id" + }, + "28": { + "nullable": false, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": false, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "", + "class_id": 2, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 1, + "fieldtype": "integer", + "help_text": "", + "internal": "automatic", + "rel_field_id": null, + "name": "class_id" + }, + "29": { + "nullable": true, + "date_update": "Fri Oct 16 11:05:04 2015", + "max_length": 128, + "icon": "0", + "optional": false, + "component": "EmField", + "string": "", + "class_id": 2, + "date_create": "Fri Oct 16 11:05:04 2015", + "name": "string", + "rank": 2, + "fieldtype": "char", + "help_text": "", + "internal": "automatic", + "rel_field_id": null, + "uniq": false + }, + "30": { + "nullable": false, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": false, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "", + "class_id": 2, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 3, + "fieldtype": "integer", + "help_text": "", + "internal": "automatic", + "rel_field_id": null, + "name": "type_id" + }, + "31": { + "nullable": false, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": false, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "", + "class_id": 2, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 4, + "fieldtype": "pk", + "help_text": "", + "internal": "automatic", + "rel_field_id": null, + "name": "lodel_id" + }, + "33": { + "nullable": false, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": false, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "", + "class_id": 13, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 1, + "fieldtype": "integer", + "help_text": "", + "internal": "automatic", + "rel_field_id": null, + "name": "class_id" + }, + "34": { + "nullable": true, + "date_update": "Fri Oct 16 11:05:04 2015", + "max_length": 128, + "icon": "0", + "optional": false, + "component": "EmField", + "string": "", + "class_id": 13, + "date_create": "Fri Oct 16 11:05:04 2015", + "name": "string", + "rank": 2, + "fieldtype": "char", + "help_text": "", + "internal": "automatic", + "rel_field_id": null, + "uniq": false + }, + "35": { + "nullable": false, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": false, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "", + "class_id": 13, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 3, + "fieldtype": "integer", + "help_text": "", + "internal": "automatic", + "rel_field_id": null, + "name": "type_id" + }, + "36": { + "nullable": false, + "date_update": "Fri Oct 16 11:05:04 2015", + "optional": false, + "icon": "0", + "uniq": false, + "component": "EmField", + "string": "", + "class_id": 13, + "date_create": "Fri Oct 16 11:05:04 2015", + "rank": 4, + "fieldtype": "pk", + "help_text": "", + "internal": "automatic", + "rel_field_id": null, + "name": "lodel_id" + }, + "37": { + "nullable": false, + "date_update": "Wed Nov 4 10:52:13 2015", + "optional": false, + "rank": 5, + "icon": "0", + "name": "modification_date", + "component": "EmField", + "string": "", + "class_id": 1, + "date_create": "Wed Nov 4 10:52:13 2015", + "now_on_create": true, + "internal": "automatic", + "fieldtype": "datetime", + "now_on_update": true, + "help_text": "", + "rel_field_id": null, + "uniq": false + }, + "38": { + "nullable": false, + "date_update": "Wed Nov 4 10:52:13 2015", + "rel_field_id": null, + "icon": "0", + "uniq": false, + "component": "EmField", + "now_on_create": true, + "class_id": 1, + "date_create": "Wed Nov 4 10:52:13 2015", + "rank": 6, + "string": "", + "help_text": "", + "internal": "automatic", + "optional": false, + "name": "creation_date", + "fieldtype": "datetime" + }, + "39": { + "nullable": false, + "date_update": "Wed Nov 4 10:52:13 2015", + "rel_field_id": null, + "icon": "0", + "uniq": false, + "component": "EmField", + "now_on_create": true, + "class_id": 2, + "date_create": "Wed Nov 4 10:52:13 2015", + "rank": 5, + "string": "", + "now_on_update": true, + "help_text": "", + "internal": "automatic", + "optional": false, + "name": "modification_date", + "fieldtype": "datetime" + }, + "40": { + "nullable": false, + "date_update": "Wed Nov 4 10:52:13 2015", + "rel_field_id": null, + "icon": "0", + "uniq": false, + "component": "EmField", + "now_on_create": true, + "class_id": 2, + "date_create": "Wed Nov 4 10:52:13 2015", + "rank": 6, + "string": "", + "help_text": "", + "internal": "automatic", + "optional": false, + "name": "creation_date", + "fieldtype": "datetime" + }, + "41": { + "nullable": false, + "date_update": "Wed Nov 4 10:52:13 2015", + "rel_field_id": null, + "icon": "0", + "uniq": false, + "component": "EmField", + "now_on_create": true, + "class_id": 13, + "date_create": "Wed Nov 4 10:52:13 2015", + "rank": 5, + "string": "", + "now_on_update": true, + "help_text": "", + "internal": "automatic", + "optional": false, + "name": "modification_date", + "fieldtype": "datetime" + }, + "42": { + "nullable": false, + "date_update": "Wed Nov 4 10:52:13 2015", + "rel_field_id": null, + "icon": "0", + "uniq": false, + "component": "EmField", + "now_on_create": true, + "class_id": 13, + "date_create": "Wed Nov 4 10:52:13 2015", + "rank": 6, + "string": "", + "help_text": "", + "internal": "automatic", + "optional": false, + "name": "creation_date", + "fieldtype": "datetime" + } +} \ No newline at end of file diff --git a/install/loader.py b/install/loader.py new file mode 100644 index 0000000..0a1cca3 --- /dev/null +++ b/install/loader.py @@ -0,0 +1,28 @@ +import settings +import importlib +import sys +import os + +sys.path.append(settings.lodel2_lib_path) + +# Import dynamic code +if os.path.isfile(settings.dynamic_code): + from dynleapi import * + +# Import wanted datasource objects +for db_modname in ['leapidatasource', 'migrationhandler']: + mod = importlib.import_module("DataSource.{pkg_name}.{mod_name}".format( + pkg_name=settings.ds_package, + mod_name=db_modname, + ) + ) + # Expose the module in globals + globals()[db_modname] = mod + +if __name__ == '__main__': + import code + print(""" + Running interactive python in Lodel2 %s instance environment + +"""%settings.name) + code.interact(local=locals()) diff --git a/install/settings.py b/install/settings.py new file mode 100644 index 0000000..477702a --- /dev/null +++ b/install/settings.py @@ -0,0 +1,21 @@ +#-*- coding:utf8 -*- + +import pymysql + +name = 'LODEL2_INSTANCE_NAME' +lodel2_lib_path = 'LODEL2_LIB_ABS_PATH' + +emfile = 'em.json' +dynamic_code = 'dynleapi.py' + +ds_package = 'MySQL' +mh_classname = 'MysqlMigrationHandler' +datasource = { + 'default': { + 'module': pymysql, + 'host': '127.0.0.1', + 'user': 'DBUSER', + 'passwd': 'DBPASSWORD', + 'db': 'DBNAME' + } +} diff --git a/install/utils.py b/install/utils.py new file mode 100755 index 0000000..a32193e --- /dev/null +++ b/install/utils.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +import settings +from settings import * +from loader import * + +def refreshdyn(): + import sys + from EditorialModel.model import Model + from leapi.lefactory import LeFactory + from EditorialModel.backend.json_backend import EmBackendJson + from DataSource.MySQL.leapidatasource import LeDataSourceSQL + OUTPUT = dynamic_code + EMJSON = emfile + # Load editorial model + em = Model(EmBackendJson(EMJSON)) + # Generate dynamic code + fact = LeFactory(OUTPUT) + # Create the python file + fact.create_pyfile(em, LeDataSourceSQL, {}) + +def db_init(): + from EditorialModel.backend.json_backend import EmBackendJson + from EditorialModel.model import Model + mh = getattr(migrationhandler,settings.mh_classname)(**(settings.datasource['default'])) + em = Model(EmBackendJson(settings.emfile)) + em.migrate_handler(mh) + + diff --git a/lodel_init.sh b/lodel_init.sh new file mode 100755 index 0000000..c6ae8ea --- /dev/null +++ b/lodel_init.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +usage() { + echo "Usage : $0 instance_name instance_dir [lodel_libdir]" 1>&2 + exit 1 +} + +if [ $# -lt 2 ] +then + echo "Not enough arguments" 1>&2 + usage +fi + + +name="$1" +instdir="$2" +libdir="$3" +libdir="${libdir:=$(realpath $(dirname $0))}" + +emfilename="em.json" +settings="$instdir/settings.py" +em="$instdir/em.json" +dyncode="$instdir/${name}.py" + +if [ -e "$instdir" ] +then + echo "Abording... "$instdir" exists" 1>&2 + exit 1 +fi + +echo "Creating lodel instance directory '$instdir'" +mkdir -pv "$instdir" + +cp -v $libdir/install/* $instdir + +sed -i -e "s#LODEL2_LIB_ABS_PATH#$libdir#" "$settings" +sed -i -e "s#LODEL2_INSTANCE_NAME#$name#" "$settings" + +echo "Generating dynamic code" +cd "$instdir" +make refreshdyn +echo "Cleaning instance directory" +make clean + +echo -e "\nInstance successfully created in $instdir" +echo -e "============================\n" +echo "Now you should edit '$settings' and then run : cd $instdir && make dbinit" diff --git a/refreshdyn.py b/refreshdyn.py index d10f356..4f25040 100755 --- a/refreshdyn.py +++ b/refreshdyn.py @@ -1,15 +1,17 @@ # -*- coding: utf-8 -*- +import sys from EditorialModel.model import Model from leapi.lefactory import LeFactory from EditorialModel.backend.json_backend import EmBackendJson from leapi.datasources.ledatasourcesql import LeDataSourceSQL -OUTPUT = 'leapi/dyn.py' +OUTPUT = 'leapi/dyn.py' if len(sys.argv) == 1 else sys.argv[1] +EMJSON = 'EditorialModel/test/me.json' if len(sys.argv) < 3 else sys.argv[2] -em = Model(EmBackendJson('EditorialModel/test/me.json')) +em = Model(EmBackendJson(EMJSON)) -fact = LeFactory('leapi/dyn.py') +fact = LeFactory(OUTPUT) fact.create_pyfile(em, LeDataSourceSQL, {}) print(fact.generate_python(em, LeDataSourceSQL, {}))