No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

migrationhandler.py 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. # -*- coding: utf-8 -*-
  2. import copy
  3. import pymysql
  4. import EditorialModel
  5. from DataSource.MySQL.common_utils import MySQL
  6. from DataSource.dummy.migrationhandler import DummyMigrationHandler
  7. # The global MH algorithm is as follow :
  8. # A create_table(table_name, pk_name, pk_opt) method that create a table
  9. # with one pk field
  10. # An add_column(table_name, field_name, field_opt) method that add a column to a table
  11. #
  12. # The create_default_table method will call both methods to create the object and relation tables
  13. #
  14. # Supported operations :
  15. # - EmClass creation
  16. # - EmClass deletion (untested)
  17. # - EmField creation
  18. # - EmField deletion (untested)
  19. # - rel2type attribute creation
  20. # - rel2type attribute deletion (unstested)
  21. #
  22. # Unsupported operations :
  23. # - EmClass rename
  24. # - EmField rename
  25. # - rel2type field rename
  26. # - rel2type attribute rename
  27. # - EmFieldType changes
  28. #
  29. # @todo Unified datasources and migration handlers via utils functions
  30. ## @brief Modify a MySQL database given editorial model changes
  31. class MysqlMigrationHandler(DummyMigrationHandler):
  32. ## @brief Construct a MysqlMigrationHandler
  33. # @param host str : The db host
  34. # @param user str : The db user
  35. # @param password str : The db password
  36. # @param db str : The db name
  37. def __init__(self, host, user, passwd, db, module=pymysql, db_engine='InnoDB', foreign_keys=True, debug=False, dryrun=False, drop_if_exists=False):
  38. self.datasource = MySQL
  39. self._dbmodule = module
  40. #Connect to MySQL
  41. self.db = self._dbmodule.connect(host=host, user=user, passwd=passwd, db=db)
  42. self.debug = debug
  43. self.dryrun = dryrun
  44. self.db_engine = db_engine
  45. self.foreign_keys = foreign_keys if db_engine == 'InnoDB' else False
  46. self.drop_if_exists = drop_if_exists
  47. #Create default tables
  48. self._create_default_tables(self.drop_if_exists)
  49. ## @brief Delete all table created by the MH
  50. # @param model Model : the Editorial model
  51. def __purge_db(self, model):
  52. for uid in [c.uid for c in model.components('EmClass')]:
  53. try:
  54. self.delete_emclass_table(model, uid)
  55. except self._dbmodule.err.InternalError as e:
  56. print(e)
  57. 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']:
  58. try:
  59. self._query("DROP TABLE %s;" % tname)
  60. except self._dbmodule.err.InternalError as e:
  61. print(e)
  62. for tname in [MySQL.relations_table_name, MySQL.objects_table_name]:
  63. try:
  64. self._query("DROP TABLE %s;" % tname)
  65. except self._dbmodule.err.InternalError as e:
  66. print(e)
  67. ## @brief Modify the db given an EM change
  68. # @param em model : The EditorialModel.model object to provide the global context
  69. # @param uid int : The uid of the change EmComponent
  70. # @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.
  71. # @param new_state dict | None : dict with field name as key and field value as value. Representing the new state. None mean component deletion
  72. # @throw EditorialModel.exceptions.MigrationHandlerChangeError if the change was refused
  73. def register_change(self, em, uid, initial_state, new_state, engine=None):
  74. if engine is None:
  75. engine = self.db_engine
  76. if isinstance(em.component(uid), EditorialModel.classes.EmClass):
  77. if initial_state is None:
  78. #EmClass creation
  79. self.create_emclass_table(em, uid, engine)
  80. elif new_state is None:
  81. #EmClass deletion
  82. self.delete_emclass_table(em, uid)
  83. elif isinstance(em.component(uid), EditorialModel.fields.EmField):
  84. emfield = em.component(uid)
  85. if emfield.rel_field_id is None:
  86. #non relationnal field
  87. if initial_state is None:
  88. #non relationnal EmField creation
  89. if emfield.name not in EditorialModel.classtypes.common_fields.keys():
  90. self.add_col_from_emfield(em, uid)
  91. elif new_state is None:
  92. #non relationnal EmField deletion
  93. if emfield.name not in EditorialModel.classtypes.common_fields.keys():
  94. self.del_col_from_emfield(em, uid)
  95. else:
  96. #relationnal field
  97. if initial_state is None:
  98. #Rel2type attr creation
  99. self.add_relationnal_field(em, uid)
  100. elif new_state is None:
  101. #Rel2type attr deletion
  102. self.del_relationnal_field(em, uid)
  103. ## @brief dumdumdummy
  104. # @note implemented to avoid the log message of EditorialModel.migrationhandler.dummy.DummyMigrationHandler
  105. def register_model_state(self, em, state_hash):
  106. pass
  107. ## @brief Exec a query
  108. # @param query str : SQL query
  109. def _query(self, query):
  110. if self.debug:
  111. print(query + "\n")
  112. if not self.dryrun:
  113. with self.db.cursor() as cur:
  114. cur.execute(query)
  115. self.db.commit() # autocommit
  116. ## @brief Add a relationnal field
  117. # Add a rel2type attribute
  118. # @note this function handles the table creation
  119. # @param em Model : EditorialModel.model.Model instance
  120. # @param rfuid int : Relationnal field uid
  121. def add_relationnal_field(self, em, rfuid):
  122. emfield = em.component(rfuid)
  123. if not isinstance(emfield, EditorialModel.fields.EmField):
  124. raise ValueError("The given uid is not an EmField uid")
  125. r2tf = em.component(emfield.rel_field_id)
  126. tname = self._r2t2table_name(em, r2tf)
  127. pkname, pkftype = self._relation_pk
  128. #If not exists create a relational table
  129. self._create_table(tname, pkname, pkftype, self.db_engine, if_exists='nothing')
  130. #Add a foreign key if wanted
  131. if self.foreign_keys:
  132. self._add_fk(tname, self.datasource.relations_table_name, pkname, pkname)
  133. #Add the column
  134. self._add_column(tname, emfield.name, emfield.fieldtype_instance())
  135. #Update table triggers
  136. self._generate_triggers(tname, self._r2type2cols(em, r2tf))
  137. ## @brief Delete a rel2type attribute
  138. #
  139. # Delete a rel2type attribute
  140. # @note this method handles the table deletion
  141. # @param em Model : EditorialModel.model.Model instance
  142. # @param rfuid int : Relationnal field uid
  143. def del_relationnal_field(self, em, rfuid):
  144. emfield = em.component(rfuid)
  145. if not isinstance(emfield, EditorialModel.fields.EmField):
  146. raise ValueError("The given uid is not an EmField uid")
  147. r2tf = em.component(emfield.rel_field_id)
  148. tname = self._r2t2table_name(em, r2tf)
  149. if len(self._r2type2cols(em, r2tf)) == 1:
  150. #The table can be deleted (no more attribute for this rel2type)
  151. self._query("""DROP TABLE {table_name}""".format(table_name=tname))
  152. else:
  153. self._del_column(tname, emfield.name)
  154. #Update table triggers
  155. self._generate_triggers(tname, self._r2type2cols(em, r2tf))
  156. ## @brief Given an EmField uid add a column to the corresponding table
  157. # @param em Model : A Model instance
  158. # @param uid int : An EmField uid
  159. def add_col_from_emfield(self, em, uid):
  160. emfield = em.component(uid)
  161. if not isinstance(emfield, EditorialModel.fields.EmField):
  162. raise ValueError("The given uid is not an EmField uid")
  163. emclass = emfield.em_class
  164. tname = self._emclass2table_name(emclass)
  165. self._add_column(tname, emfield.name, emfield.fieldtype_instance())
  166. # Refresh the table triggers
  167. cols_l = self._class2cols(emclass)
  168. self._generate_triggers(tname, cols_l)
  169. ## @brief Given a class uid create the coressponding table
  170. # @param em Model : A Model instance
  171. # @param uid int : An EmField uid
  172. def create_emclass_table(self, em, uid, engine):
  173. emclass = em.component(uid)
  174. if not isinstance(emclass, EditorialModel.classes.EmClass):
  175. raise ValueError("The given uid is not an EmClass uid")
  176. pkname, pktype = self._common_field_pk
  177. table_name = self._emclass2table_name(emclass)
  178. self._create_table(table_name, pkname, pktype, engine=engine)
  179. if self.foreign_keys:
  180. self._add_fk(table_name, self.datasource.objects_table_name, pkname, pkname)
  181. ## @brief Given an EmClass uid delete the corresponding table
  182. # @param em Model : A Model instance
  183. # @param uid int : An EmField uid
  184. def delete_emclass_table(self, em, uid):
  185. emclass = em.component(uid)
  186. if not isinstance(emclass, EditorialModel.classes.EmClass):
  187. raise ValueError("The give uid is not an EmClass uid")
  188. tname = self._emclass2table_name(emclass)
  189. # Delete the table triggers to prevent errors
  190. self._generate_triggers(tname, dict())
  191. tname = self.datasource.escape_idname(tname)
  192. self._query("""DROP TABLE {table_name};""".format(table_name=tname))
  193. ## @brief Given an EmField delete the corresponding column
  194. # @param em Model : an @ref EditorialModel.model.Model instance
  195. # @param uid int : an EmField uid
  196. def delete_col_from_emfield(self, em, uid):
  197. emfield = em.component(uid)
  198. if not isinstance(emfield, EditorialModel.fields.EmField):
  199. raise ValueError("The given uid is not an EmField uid")
  200. emclass = emfield.em_class
  201. tname = self._emclass2table_name(emclass)
  202. # Delete the table triggers to prevent errors
  203. self._generate_triggers(tname, dict())
  204. self._del_column(tname, emfield.name)
  205. # Refresh the table triggers
  206. cols_ls = self._class2cols(emclass)
  207. self._generate_triggers(tname, cols_l)
  208. ## @brief Delete a column from a table
  209. # @param tname str : The table name
  210. # @param fname str : The column name
  211. def _del_column(self, tname, fname):
  212. tname = self.datasource.escape_idname(tname)
  213. fname = self.datasource.escape_idname(fname)
  214. self._query("""ALTER TABLE {table_name} DROP COLUMN {col_name};""".format(table_name=tname, col_name=fname))
  215. ## @brief Construct a table name given an EmClass instance
  216. # @param emclass EmClass : An EmClass instance
  217. # @return a table name
  218. def _emclass2table_name(self, emclass):
  219. return self.datasource.get_table_name_from_class(emclass.name)
  220. #return "class_%s"%emclass.name
  221. ## @brief Construct a table name given a rela2type EmField instance
  222. # @param em Model : A Model instance
  223. # @param emfield EmField : An EmField instance
  224. # @return a table name
  225. def _r2t2table_name(self, em, emfield):
  226. emclass = emfield.em_class
  227. emtype = em.component(emfield.rel_to_type_id)
  228. return self.datasource.get_r2t2table_name(emclass.name, emtype.name)
  229. #return "%s_%s_%s"%(emclass.name, emtype.name, emfield.name)
  230. ## @brief Generate a columns_fieldtype dict given a rel2type EmField
  231. # @param em Model : an @ref EditorialModel.model.Model instance
  232. # @param emfield EmField : and @ref EditorialModel.fields.EmField instance
  233. def _r2type2cols(self, em, emfield):
  234. return {f.name: f.fieldtype_instance() for f in em.components('EmField') if f.rel_field_id == emfield.uid}
  235. ## @brief Generate a columns_fieldtype dict given an EmClass
  236. # @param emclass EmClass : An EmClass instance
  237. # @return A dict with column name as key and EmFieldType instance as value
  238. def _class2cols(self, emclass):
  239. if not isinstance(emclass, EditorialModel.classes.EmClass):
  240. raise ValueError("The given uid is not an EmClass uid")
  241. return {f.name: f.fieldtype_instance() for f in emclass.fields() if f.name not in EditorialModel.classtypes.common_fields.keys()}
  242. ## @brief Create object and relations tables
  243. # @param drop_if_exist bool : If true drop tables if exists
  244. def _create_default_tables(self, drop_if_exist=False):
  245. if_exists = 'drop' if drop_if_exist else 'nothing'
  246. #Object tablea
  247. tname = self.datasource.objects_table_name
  248. pk_name, pk_ftype = self._common_field_pk
  249. self._create_table(tname, pk_name, pk_ftype, engine=self.db_engine, if_exists=if_exists)
  250. #Adding columns
  251. cols = {fname: self._common_field_to_ftype(fname) for fname in EditorialModel.classtypes.common_fields}
  252. for fname, ftype in cols.items():
  253. if fname != pk_name:
  254. self._add_column(tname, fname, ftype)
  255. #Creating triggers
  256. self._generate_triggers(tname, cols)
  257. #Relation table
  258. tname = self.datasource.relations_table_name
  259. pk_name, pk_ftype = self._relation_pk
  260. self._create_table(tname, pk_name, pk_ftype, engine=self.db_engine, if_exists=if_exists)
  261. #Adding columns
  262. for fname, ftype in self._relation_cols.items():
  263. self._add_column(tname, fname, ftype)
  264. #Creating triggers
  265. self._generate_triggers(tname, self._relation_cols)
  266. ## @return true if the name changes
  267. def _name_change(self, initial_state, new_state):
  268. return 'name' in initial_state and initial_state['name'] != new_state['name']
  269. ## @brief Create a table with primary key
  270. # @param table_name str : table name
  271. # @param pk_name str : pk column name
  272. # @param pk_specs str : see @ref _field_to_sql()
  273. # @param engine str : The engine to use with this table
  274. # @param charset str : The charset of this table
  275. # @param if_exist str : takes values in ['nothing', 'drop']
  276. def _create_table(self, table_name, pk_name, pk_ftype, engine, charset='utf8', if_exists='nothing'):
  277. #Escaped table name
  278. etname = self.datasource.escape_idname(table_name)
  279. pk_type = self._field_to_type(pk_ftype)
  280. pk_specs = self._field_to_specs(pk_ftype)
  281. if if_exists == 'drop':
  282. self._query("""DROP TABLE IF EXISTS {table_name};""".format(table_name=etname))
  283. qres = """
  284. CREATE TABLE {table_name} (
  285. {pk_name} {pk_type} {pk_specs},
  286. PRIMARY KEY({pk_name})
  287. ) ENGINE={engine} DEFAULT CHARSET={charset};"""
  288. elif if_exists == 'nothing':
  289. qres = """CREATE TABLE IF NOT EXISTS {table_name} (
  290. {pk_name} {pk_type} {pk_specs},
  291. PRIMARY KEY({pk_name})
  292. ) ENGINE={engine} DEFAULT CHARSET={charset};"""
  293. else:
  294. raise ValueError("Unexpected value for argument if_exists '%s'." % if_exists)
  295. self._query(qres.format(
  296. table_name=self.datasource.escape_idname(table_name),
  297. pk_name=self.datasource.escape_idname(pk_name),
  298. pk_type=pk_type,
  299. pk_specs=pk_specs,
  300. engine=engine,
  301. charset=charset
  302. ))
  303. ## @brief Add a column to a table
  304. # @param table_name str : The table name
  305. # @param col_name str : The columns name
  306. # @param col_fieldtype EmFieldype the fieldtype
  307. def _add_column(self, table_name, col_name, col_fieldtype, drop_if_exists=False):
  308. add_col = """ALTER TABLE {table_name}
  309. ADD COLUMN {col_name} {col_type} {col_specs};"""
  310. etname = self.datasource.escape_idname(table_name)
  311. ecname = self.datasource.escape_idname(col_name)
  312. add_col = add_col.format(
  313. table_name=etname,
  314. col_name=ecname,
  315. col_type=self._field_to_type(col_fieldtype),
  316. col_specs=self._field_to_specs(col_fieldtype),
  317. )
  318. try:
  319. self._query(add_col)
  320. except self._dbmodule.err.InternalError as e:
  321. if drop_if_exists:
  322. self._del_column(table_name, col_name)
  323. self._add_column(table_name, col_name, col_fieldtype, drop_if_exists)
  324. else:
  325. #LOG
  326. print("Aborded, column `%s` exists" % col_name)
  327. ## @brief Add a foreign key
  328. # @param src_table_name str : The name of the table where we will add the FK
  329. # @param dst_table_name str : The name of the table the FK will point on
  330. # @param src_col_name str : The name of the concerned column in the src_table
  331. # @param dst_col_name str : The name of the concerned column in the dst_table
  332. def _add_fk(self, src_table_name, dst_table_name, src_col_name, dst_col_name):
  333. stname = self.datasource.escape_idname(src_table_name)
  334. dtname = self.datasource.escape_idname(dst_table_name)
  335. scname = self.datasource.escape_idname(src_col_name)
  336. dcname = self.datasource.escape_idname(dst_col_name)
  337. fk_name = self.datasource.get_fk_name(src_table_name, dst_table_name)
  338. self._del_fk(src_table_name, dst_table_name)
  339. self._query("""ALTER TABLE {src_table}
  340. ADD CONSTRAINT {fk_name}
  341. FOREIGN KEY ({src_col}) references {dst_table}({dst_col});""".format(
  342. fk_name=self.datasource.escape_idname(fk_name),
  343. src_table=stname,
  344. src_col=scname,
  345. dst_table=dtname,
  346. dst_col=dcname
  347. ))
  348. ## @brief Given a source and a destination table, delete the corresponding FK
  349. # @param src_table_name str : The name of the table where the FK is
  350. # @param dst_table_name str : The name of the table the FK point on
  351. # @warning fails silently
  352. def _del_fk(self, src_table_name, dst_table_name):
  353. try:
  354. self._query("""ALTER TABLE {src_table}
  355. DROP FOREIGN KEY {fk_name}""".format(
  356. src_table=self.datasource.escape_idname(src_table_name),
  357. fk_name=self.datasource.escape_idname(self.datasource.get_fk_name(src_table_name, dst_table_name))
  358. ))
  359. except self._dbmodule.err.InternalError:
  360. # If the FK don't exists we do not care
  361. pass
  362. ## @brief Generate triggers given a table_name and its columns fieldtypes
  363. # @param table_name str : Table name
  364. # @param cols_ftype dict : with col name as key and column fieldtype as value
  365. def _generate_triggers(self, table_name, cols_ftype):
  366. colval_l_upd = dict() # param for update trigger
  367. colval_l_ins = dict() # param for insert trigger
  368. for cname, cftype in cols_ftype.items():
  369. if cftype.ftype == 'datetime':
  370. if cftype.now_on_update:
  371. colval_l_upd[cname] = 'NOW()'
  372. if cftype.now_on_create:
  373. colval_l_ins[cname] = 'NOW()'
  374. self._table_trigger(table_name, 'UPDATE', colval_l_upd)
  375. self._table_trigger(table_name, 'INSERT', colval_l_ins)
  376. ## @brief Create trigger for a table
  377. #
  378. # Primarly designed to create trigger for DATETIME types
  379. # The method generates triggers of the form
  380. #
  381. # CREATE TRIGGER BEFORE <moment> ON <table_name>
  382. # FOR EACH ROW SET <for colname, colval in cols_val>
  383. # NEW.<colname> = <colval>,
  384. # <endfor>;
  385. # @param table_name str : The table name
  386. # @param moment str : can be 'update' or 'insert'
  387. # @param cols_val dict : Dict with column name as key and column value as value
  388. def _table_trigger(self, table_name, moment, cols_val):
  389. trigger_name = self.datasource.escape_idname("%s_%s_trig" % (table_name, moment))
  390. #Try to delete the trigger
  391. drop_trig = """DROP TRIGGER IF EXISTS {trigger_name};""".format(trigger_name=trigger_name)
  392. self._query(drop_trig)
  393. col_val_l = ', '.join(["NEW.%s = %s" % (self.datasource.escape_idname(cname), cval)for cname, cval in cols_val.items()])
  394. #Create a trigger if needed
  395. if len(col_val_l) > 0:
  396. trig_q = """CREATE TRIGGER {trigger_name} BEFORE {moment} ON {table_name}
  397. FOR EACH ROW SET {col_val_list};""".format(
  398. trigger_name=trigger_name,
  399. table_name=self.datasource.escape_idname(table_name),
  400. moment=moment, col_val_list=col_val_l
  401. )
  402. self._query(trig_q)
  403. ## @brief Identifier escaping
  404. # @param idname str : An SQL identifier
  405. #def _idname_escape(self, idname):
  406. # if '`' in idname:
  407. # raise ValueError("Invalid name : '%s'"%idname)
  408. # return '`%s`'%idname
  409. ## @brief Returns column specs from fieldtype
  410. # @param emfieldtype EmFieldType : An EmFieldType insance
  411. # @todo escape default value
  412. def _field_to_specs(self, emfieldtype):
  413. colspec = ''
  414. if not emfieldtype.nullable:
  415. colspec = 'NOT NULL'
  416. if hasattr(emfieldtype, 'default'):
  417. colspec += ' DEFAULT '
  418. if emfieldtype.default is None:
  419. colspec += 'NULL '
  420. else:
  421. colspec += emfieldtype.default # ESCAPE VALUE HERE !!!!
  422. if emfieldtype.name == 'pk':
  423. colspec += ' AUTO_INCREMENT'
  424. return colspec
  425. ## @brief Given a fieldtype return a MySQL type specifier
  426. # @param emfieldtype EmFieldType : A fieldtype
  427. # @return the corresponding MySQL type
  428. def _field_to_type(self, emfieldtype):
  429. ftype = emfieldtype.ftype
  430. if ftype == 'char' or ftype == 'str':
  431. res = "VARCHAR(%d)" % emfieldtype.max_length
  432. elif ftype == 'text':
  433. res = "TEXT"
  434. elif ftype == 'datetime':
  435. res = "DATETIME"
  436. # client side workaround for only one column with CURRENT_TIMESTAMP : giving NULL to timestamp that don't allows NULL
  437. # cf. https://dev.mysql.com/doc/refman/5.0/en/timestamp-initialization.html#idm139961275230400
  438. # The solution for the migration handler is to create triggers :
  439. # CREATE TRIGGER trigger_name BEFORE INSERT ON `my_super_table`
  440. # FOR EACH ROW SET NEW.my_date_column = NOW();
  441. # and
  442. # CREATE TRIGGER trigger_name BEFORE UPDATE ON
  443. elif ftype == 'bool':
  444. res = "BOOL"
  445. elif ftype == 'int':
  446. res = "INT"
  447. elif ftype == 'rel2type':
  448. res = "INT"
  449. else:
  450. raise ValueError("Unsuported fieldtype ftype : %s" % ftype)
  451. return res
  452. ## @brief Returns a tuple (pkname, pk_ftype)
  453. @property
  454. def _common_field_pk(self):
  455. for fname, fta in EditorialModel.classtypes.common_fields.items():
  456. if fta['fieldtype'] == 'pk':
  457. return (fname, self._common_field_to_ftype(fname))
  458. return (None, None)
  459. ## @brief Returns a tuple (rel_pkname, rel_ftype)
  460. # @todo do it
  461. @property
  462. def _relation_pk(self):
  463. return (MySQL.relations_pkname, EditorialModel.fieldtypes.pk.EmFieldType())
  464. ## @brief Returns a dict { colname:fieldtype } of relation table columns
  465. @property
  466. def _relation_cols(self):
  467. from_name = EditorialModel.fieldtypes.generic.GenericFieldType.from_name
  468. return {
  469. 'id_sup': from_name('integer')(),
  470. 'id_sub': from_name('integer')(),
  471. 'rank': from_name('integer')(nullable=True),
  472. 'depth': from_name('integer')(nullable=True),
  473. 'nature': from_name('char')(max_lenght=10, nullable=True),
  474. }
  475. ## @brief Given a common field name return an EmFieldType instance
  476. # @param cname str : Common field name
  477. # @return An EmFieldType instance
  478. def _common_field_to_ftype(self, cname):
  479. fta = copy.copy(EditorialModel.classtypes.common_fields[cname])
  480. fto = EditorialModel.fieldtypes.generic.GenericFieldType.from_name(fta['fieldtype'])
  481. del fta['fieldtype']
  482. return fto(**fta)