暫無描述
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.

leapidatasource.py 39KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852
  1. #-*- coding: utf-8 -*-
  2. import pymysql
  3. import copy
  4. import leapi
  5. from leapi.leobject import REL_SUB, REL_SUP
  6. from leapi.lecrud import _LeCrud
  7. from mosql.db import Database, all_to_dicts, one_to_dict
  8. from mosql.query import select, insert, update, delete, join, left_join
  9. from mosql.util import raw, or_
  10. import mosql.mysql
  11. from DataSource.dummy.leapidatasource import DummyDatasource
  12. from DataSource.MySQL import utils
  13. from EditorialModel.classtypes import EmNature
  14. from Lodel.settings import Settings
  15. ## MySQL DataSource for LeObject
  16. class LeDataSourceSQL(DummyDatasource):
  17. RELATIONS_POSITIONS_FIELDS = {REL_SUP: 'superior_id', REL_SUB: 'subordinate_id'}
  18. def __init__(self, module=pymysql, conn_args=None):
  19. super(LeDataSourceSQL, self).__init__()
  20. self.module = module
  21. if conn_args is None:
  22. conn_args = copy.copy(Settings.get('datasource')['default'])
  23. self.module = conn_args['module']
  24. del conn_args['module']
  25. self.connection = Database(self.module, **conn_args)
  26. ## @brief select lodel editorial components using given filters
  27. # @param target_cls LeCrud(class): The component class concerned by the select (a LeCrud child class (not instance !) )
  28. # @param field_list list: List of field to fetch
  29. # @param filters list: List of filters (see @ref lecrud_filters)
  30. # @param rel_filters list: List of relational filters (see @ref lecrud_filters)
  31. # @return a list of LeCrud child classes
  32. # @todo this only works with LeObject.get(), LeClass.get() and LeType.get()
  33. # @todo for speed get rid of all_to_dicts
  34. # @todo filters: all use cases are not implemented
  35. # @todo group: mosql does not permit direction in group_by clause, it should, so for now we don't use direction in group clause
  36. def select(self, target_cls, field_list, filters, rel_filters=None, order=None, group=None, limit=None, offset=None, instanciate=True):
  37. joins = []
  38. # it is a LeObject, query only on main table
  39. if target_cls.__name__ == 'LeObject':
  40. main_table = utils.common_tables['object']
  41. fields = [(main_table, target_cls.fieldlist())]
  42. # it is a LeType or a LeClass, query on main table left join class table on lodel_id
  43. elif target_cls.is_letype() or target_cls.is_leclass():
  44. # find main table
  45. main_table = utils.common_tables['object']
  46. main_class = target_cls.leo_class()
  47. # find class table
  48. class_table = utils.object_table_name(main_class.__name__)
  49. main_lodel_id = utils.column_prefix(main_table, main_class.uidname())
  50. class_lodel_id = utils.column_prefix(class_table, main_class.uidname())
  51. # do the join
  52. joins = [left_join(class_table, {main_lodel_id:class_lodel_id})]
  53. fields = [(main_table, target_cls.name2class('LeObject').fieldlist()), (class_table, main_class.fieldlist())]
  54. elif target_cls.is_lehierarch():
  55. main_table = utils.common_tables['relation']
  56. fields = [(main_table, target_cls.name2class('LeRelation').fieldlist())]
  57. elif target_cls.is_lerel2type():
  58. # find main table
  59. main_table = utils.common_tables['relation']
  60. # find relational table
  61. class_table = utils.r2t_table_name(target_cls._superior_cls.__name__, target_cls._subordinate_cls.__name__)
  62. relation_fieldname = target_cls.name2class('LeRelation').uidname()
  63. main_relation_id = utils.column_prefix(main_table, relation_fieldname)
  64. class_relation_id = utils.column_prefix(class_table, relation_fieldname)
  65. # do the joins
  66. lodel_id = target_cls.name2class('LeObject').uidname()
  67. joins = [
  68. left_join(class_table, {main_relation_id:class_relation_id}),
  69. # join with the object table to retrieve class and type of superior and subordinate
  70. left_join(utils.common_tables['object'] + ' as sup_obj', {'sup_obj.'+lodel_id:target_cls._superior_field_name}),
  71. left_join(utils.common_tables['object'] + ' as sub_obj', {'sub_obj.'+lodel_id:target_cls._subordinate_field_name})
  72. ]
  73. fields = [
  74. (main_table, target_cls.name2class('LeRelation').fieldlist()),
  75. (class_table, target_cls.fieldlist()),
  76. ('sup_obj', ['class_id']),
  77. ('sub_obj', ['type_id'])
  78. ]
  79. field_list.extend(['class_id', 'type_id'])
  80. else:
  81. raise AttributeError("Target class '%s' in get() is not a Lodel Editorial Object !" % target_cls)
  82. # prefix column name in fields list
  83. prefixed_field_list = [utils.find_prefix(name, fields) for name in field_list]
  84. kwargs = {}
  85. if group:
  86. kwargs['group_by'] = (utils.find_prefix(column, fields) for column, direction in group)
  87. if order:
  88. kwargs['order_by'] = (utils.find_prefix(column, fields) + ' ' + direction for column, direction in order)
  89. if limit:
  90. kwargs['limit'] = limit
  91. if offset:
  92. kwargs['offset'] = offset
  93. # @todo implement relational filters
  94. # prefix filters'' column names, and prepare dict for mosql where {(fieldname, op): value}
  95. wheres = {(utils.find_prefix(name, fields), op):value for name,op,value in filters}
  96. query = select(main_table, select=prefixed_field_list, where=wheres, joins=joins, **kwargs)
  97. # Executing the query
  98. cur = utils.query(self.connection, query)
  99. results = all_to_dicts(cur)
  100. #print(results)
  101. # instanciate each row to editorial components
  102. if instanciate:
  103. results = [target_cls.object_from_data(datas) for datas in results]
  104. #print('results', results)
  105. return results
  106. ## @brief delete lodel editorial components given filters
  107. # @param target_cls LeCrud(class): The component class concerned by the delete (a LeCrud child class (not instance !) )
  108. # @param filters list : List of filters (see @ref leobject_filters)
  109. # @param rel_filters list : List of relational filters (see @ref leobject_filters)
  110. # @return the number of deleted components
  111. def delete(self, target_cls, filters, rel_filters):
  112. query_table_name = self.datasource_utils.get_table_name_from_class(target_cls.__name__)
  113. prep_filters = self._prepare_filters(filters, query_table_name)
  114. prep_rel_filters = self._prepare_rel_filters(rel_filters)
  115. if len(prep_rel_filters) > 0:
  116. query = "DELETE %s FROM" % query_table_name
  117. for prep_rel_filter in prep_rel_filters:
  118. query += "%s INNER JOIN %s ON (%s.%s = %s.%s)" % (
  119. self.datasource_utils.relations_table_name,
  120. query_table_name,
  121. self.datasource_utils.relations_table_name,
  122. prep_rel_filter['position'],
  123. query_table_name,
  124. self.datasource_utils.field_lodel_id
  125. )
  126. if prep_rel_filter['condition_key'][0] is not None:
  127. prep_filters[("%s.%s" % (self.datasource_utils.relations_table_name, prep_rel_filter['condition_key'][0]), prep_rel_filter['condition_key'][1])] = prep_rel_filter['condition_value']
  128. if prep_filters is not None and len(prep_filters) > 0:
  129. query += " WHERE "
  130. filter_counter = 0
  131. for filter_item in prep_filters:
  132. if filter_counter > 1:
  133. query += " AND "
  134. query += "%s %s %s" % (filter_item[0][0], filter_item[0][1], filter_item[1])
  135. else:
  136. query = delete(query_table_name, prep_filters)
  137. query_delete_from_object = delete(self.datasource_utils.objects_table_name, {'lodel_id': filters['lodel_id']})
  138. with self.connection as cur:
  139. result = cur.execute(query)
  140. cur.execute(query_delete_from_object)
  141. return result
  142. ## @brief update an existing lodel editorial component
  143. # @param target_cls LeCrud(class) : Instance of the object concerned by the update
  144. # @param filters list : List of filters (see @ref leobject_filters)
  145. # @param rel_filters list : List of relationnal filters (see @ref leobject_filters)
  146. # @param **datas : Datas in kwargs
  147. # @return the number of updated components
  148. # @todo implement other filters than lodel_id
  149. def update(self, target_cls, filters, rel_filters, **datas):
  150. class_table = False
  151. # it is a LeType
  152. if target_cls.is_letype():
  153. # find main table and main table datas
  154. main_table = utils.common_tables['object']
  155. main_datas = {target_cls.uidname(): raw(target_cls.uidname())} # be sure to have one SET clause
  156. main_fields = target_cls.name2class('LeObject').fieldlist()
  157. class_table = utils.object_table_name(target_cls.leo_class().__name__)
  158. elif target_cls.is_lerel2type():
  159. main_table = utils.common_tables['relation']
  160. le_relation = target_cls.name2class('LeRelation')
  161. main_datas = {le_relation.uidname(): raw(le_relation.uidname())} # be sure to have one SET clause
  162. main_fields = le_relation.fieldlist()
  163. class_table = utils.r2t_table_name(target_cls._superior_cls.__name__, target_cls._subordinate_cls.__name__)
  164. else:
  165. raise AttributeError("'%s' is not a LeType nor a LeRelation, it's impossible to update it" % target_cls)
  166. for main_column_name in main_fields:
  167. if main_column_name in datas:
  168. main_datas[main_column_name] = datas[main_column_name]
  169. del(datas[main_column_name])
  170. wheres = {(name, op):value for name,op,value in filters}
  171. sql = update(main_table, wheres, main_datas)
  172. utils.query(self.connection, sql)
  173. # update on class table
  174. if class_table and datas:
  175. sql = update(class_table, wheres, datas)
  176. utils.query(self.connection, sql)
  177. return True
  178. ## @brief inserts a new lodel editorial component
  179. # @param target_cls LeCrud(class) : The component class concerned by the insert (a LeCrud child class (not instance !) )
  180. # @param **datas : The datas to insert
  181. # @return The inserted component's id
  182. # @todo should work with LeType, LeClass, and Relations
  183. def insert(self, target_cls, **datas):
  184. class_table = False
  185. # it is a LeType
  186. if target_cls.is_letype():
  187. main_table = utils.common_tables['object']
  188. main_datas = {'class_id':target_cls.leo_class()._class_id, 'type_id':target_cls._type_id}
  189. main_fields = target_cls.name2class('LeObject').fieldlist()
  190. class_table = utils.object_table_name(target_cls.leo_class().__name__)
  191. fk_name = target_cls.uidname()
  192. # it is a hierarchy
  193. elif target_cls.is_lehierarch():
  194. main_table = utils.common_tables['relation']
  195. main_datas = {
  196. utils.column_name(target_cls._superior_field_name): datas[target_cls._superior_field_name].lodel_id,
  197. utils.column_name(target_cls._subordinate_field_name): datas[target_cls._subordinate_field_name].lodel_id
  198. }
  199. main_fields = target_cls.name2class('LeRelation').fieldlist()
  200. # it is a relation
  201. elif target_cls.is_lerel2type():
  202. main_table = utils.common_tables['relation']
  203. main_datas = {
  204. utils.column_name(target_cls._superior_field_name): datas[target_cls._superior_field_name].lodel_id,
  205. utils.column_name(target_cls._subordinate_field_name): datas[target_cls._subordinate_field_name].lodel_id
  206. }
  207. main_fields = target_cls.name2class('LeRelation').fieldlist()
  208. superior_class = datas['superior'].leo_class()
  209. class_table = utils.r2t_table_name(superior_class.__name__, datas['subordinate'].__class__.__name__)
  210. fk_name = superior_class.name2class('LeRelation').uidname()
  211. else:
  212. raise AttributeError("'%s' is not a LeType nor a LeRelation, it's impossible to insert it" % target_cls)
  213. # extract main table datas from datas
  214. for main_column_name in main_fields:
  215. if main_column_name in datas:
  216. if main_column_name not in main_datas:
  217. main_datas[main_column_name] = datas[main_column_name]
  218. del(datas[main_column_name])
  219. sql = insert(main_table, main_datas)
  220. cur = utils.query(self.connection, sql)
  221. lodel_id = cur.lastrowid
  222. if class_table:
  223. # insert in class_table
  224. datas[fk_name] = lodel_id
  225. sql = insert(class_table, datas)
  226. utils.query(self.connection, sql)
  227. return lodel_id
  228. ## @brief insert multiple editorial component
  229. # @param target_cls LeCrud(class) : The component class concerned by the insert (a LeCrud child class (not instance !) )
  230. # @param datas_list list : A list of dict representing the datas to insert
  231. # @return int the number of inserted component
  232. def insert_multi(self, target_cls, datas_list):
  233. res = list()
  234. for data in datas_list:
  235. res.append(self.insert(target_cls, data))
  236. return len(res)
  237. ## @brief prepares the relational filters
  238. # @params rel_filters : (("superior"|"subordinate"), operator, value)
  239. # @return list
  240. def _prepare_rel_filters(self, rel_filters):
  241. prepared_rel_filters = []
  242. if rel_filters is not None and len(rel_filters) > 0:
  243. for rel_filter in rel_filters:
  244. rel_filter_dict = {
  245. 'position': REL_SUB if rel_filter[0][0] == REL_SUP else REL_SUB,
  246. 'nature': rel_filter[0][1],
  247. 'condition_key': (self.RELATIONS_POSITIONS_FIELDS[rel_filter[0][0]], rel_filter[1]),
  248. 'condition_value': rel_filter[2]
  249. }
  250. prepared_rel_filters.append(rel_filter_dict)
  251. return prepared_rel_filters
  252. ## @brief prepares the filters to be used by the mosql library's functions
  253. # @params filters : (FIELD, OPERATOR, VALUE) tuples
  254. # @return dict : Dictionnary with (FIELD, OPERATOR):VALUE style elements
  255. def _prepare_filters(self, filters, tablename=None):
  256. prepared_filters = {}
  257. if filters is not None and len(filters) > 0:
  258. for filter_item in filters:
  259. if '.' in filter_item[0]:
  260. prepared_filter_key = (filter_item[0], filter_item[1])
  261. else:
  262. prepared_filter_key = ("%s.%s" % (tablename, filter_item[0]), filter_item[1])
  263. prepared_filter_value = filter_item[2]
  264. prepared_filters[prepared_filter_key] = prepared_filter_value
  265. return prepared_filters
  266. # ================================================================================================================ #
  267. # FONCTIONS A DEPLACER #
  268. # ================================================================================================================ #
  269. ## @brief Make a relation between 2 LeType
  270. # @note rel2type relations. Superior is the LeType from the EmClass and subordinate the LeType for the EmType
  271. # @param lesup LeType : LeType child class instance that is from the EmClass containing the rel2type field
  272. # @param lesub LeType : LeType child class instance that is from the EmType linked by the rel2type field ( @ref EditorialModel.fieldtypes.rel2type.EmFieldType.rel_to_type_id )
  273. # @return The relation_id if success else return False
  274. def add_related(self, lesup, lesub, rank, **rel_attr):
  275. with self.connection as cur:
  276. #First step : relation table insert
  277. sql = insert(MySQL.relations_table_name, {
  278. 'id_sup': lesup.lodel_id,
  279. 'id_sub': lesub.lodel_id,
  280. 'rank': 0, # default value that will be set latter
  281. })
  282. cur.execute(sql)
  283. relation_id = cur.lastrowid
  284. if len(rel_attr) > 0:
  285. #There is some relation attribute to add in another table
  286. attr_table = get_r2t2table_name(lesup._leclass.__name__, lesub.__class__.__name__)
  287. rel_attr['id_relation'] = relation_id
  288. sql = insert(attr_table, rel_attr)
  289. cur.execute(sql)
  290. self._set_relation_rank(id_relation, rank)
  291. return relation_id
  292. ## @brief Deletes the relation between 2 LeType
  293. # @param lesup LeType
  294. # @param lesub LeType
  295. # @param fields dict
  296. # @return True if success else False
  297. # @todo Add fields parameter to identify relation
  298. # @todo Delete relationnal fields if some exists
  299. def del_related(self, lesup, lesub, fields=None):
  300. with self.connection as cur:
  301. del_params = {
  302. 'id_sup': lesup.lodel_id,
  303. 'id_sub': lesub.lodel_id
  304. }
  305. delete_params = {}
  306. if fields is not None:
  307. delete_params = del_params.copy()
  308. delete_params.update(fields)
  309. else:
  310. delete_params = del_params
  311. sql = delete(
  312. self.datasource_utils.relations_table_name,
  313. delete_params
  314. )
  315. if cur.execute(sql) != 1:
  316. return False
  317. return True
  318. ## @brief Fetch related (rel2type) by LeType
  319. # @param leo LeType : We want related LeObject of this LeType child class instance
  320. # @param letype LeType(class) : We want related LeObject of this LeType child class (not instance)
  321. # @param get_sub bool : If True leo is the superior and we want subordinates, else its the opposite
  322. # @return a list of dict { 'id_relation':.., 'rank':.., 'lesup':.., 'lesub'.., 'rel_attrs': dict() }
  323. # TODO A conserver , utilisé par la nouvelle méthode update_rank
  324. def get_related(self, leo, letype, get_sub=True):
  325. if LeCrud.name2class('LeType') not in letype.__bases__:
  326. raise ValueError("letype argument should be a LeType child class, but got %s" % type(letype))
  327. if not isinstance(leo, LeType):
  328. raise ValueError("leo argument should be a LeType child class instance but got %s" % type(leo))
  329. with self.connection as cur:
  330. id_leo, id_type = 'id_sup', 'id_sub' if get_sub else 'id_sub', 'id_sup'
  331. joins = [
  332. join(
  333. (MySQL.objects_table_name, 'o'),
  334. on={'r.' + id_type: 'o.' + MySQL.field_lodel_id}
  335. ),
  336. join(
  337. (MySQL.objects_table_name, 'p'),
  338. on={'r.' + id_leo: 'p.' + MySQL.field_lodel_id}
  339. ),
  340. ]
  341. lesup, lesub = leo.__class__, letype if get_sub else letype, leo.__class__
  342. common_infos = ('r.id_relation', 'r.id_sup', 'r.id_sub', 'r.rank', 'r.depth')
  343. if len(lesup._linked_types[lesub]) > 0:
  344. #relationnal attributes, need to join with r2t table
  345. cls_name = leo.__class__.__name__ if get_sub else letype.__name__
  346. type_name = letype.__name__ if get_sub else leo.__class__.__name__
  347. joins.append(
  348. join(
  349. (MySQL.get_r2t2table_name(cls_name, type_name), 'r2t'),
  350. on={'r.' + MySQL.relations_pkname: 'r2t' + MySQL.relations_pkname}
  351. )
  352. )
  353. select = ('r.id_relation', 'r.id_sup', 'r.id_sub', 'r.rank', 'r.depth', 'r2t.*')
  354. else:
  355. select = common_infos
  356. sql = select(
  357. (MySQL.relations_table_name, 'r'),
  358. select=select,
  359. where={
  360. id_leo: leo.lodel_id,
  361. 'type_id': letype._type_id,
  362. },
  363. joins=joins
  364. )
  365. cur.execute(sql)
  366. res = all_to_dicts(cur)
  367. #Building result
  368. ret = list()
  369. for datas in res:
  370. r_letype = letype(res['r.' + id_type])
  371. ret_item = {
  372. 'id_relation': +datas[MySQL.relations_pkname],
  373. 'lesup': r_leo if get_sub else r_letype,
  374. 'lesub': r_letype if get_sub else r_leo,
  375. 'rank': res['rank']
  376. }
  377. rel_attr = copy.copy(datas)
  378. for todel in common_infos:
  379. del rel_attr[todel]
  380. ret_item['rel_attrs'] = rel_attr
  381. ret.append(ret_item)
  382. return ret
  383. ## @brief Set the rank of a relation identified by its ID
  384. # @param id_relation int : relation ID
  385. # @param rank int|str : 'first', 'last', or an integer value
  386. # @throw ValueError if rank is not valid
  387. # @throw leapi.leapi.LeObjectQueryError if id_relation don't exists
  388. def set_relation_rank(self, id_relation, rank):
  389. self._check_rank(rank)
  390. self._set_relation_rank(id_relation, rank)
  391. ## @brief Sets a new rank on a relation
  392. # @param le_relation LeRelation
  393. # @param new_rank int: integer representing the absolute new rank
  394. # @return True if success, False if failure
  395. # TODO Conserver cette méthode dans le datasource du fait des requêtes SQL. Elle est appelée par le set_rank de LeRelation
  396. def update_rank(self, le_relation, rank):
  397. lesup = le_relation.id_sup
  398. lesub = le_relation.id_sub
  399. current_rank = le_relation.rank
  400. relations = le_relation.__class__.get(query_filters=[('id_sup', '=', lesup)], order=[('rank', 'ASC')])
  401. # relations = self.get_related(lesup, lesub.__class__, get_sub=True)
  402. # insert the relation at the right position considering its new rank
  403. our_relation = relations.pop(current_rank)
  404. relations.insert(our_relation, rank)
  405. # rebuild now the list of relations from the resorted list and recalculating the ranks
  406. rdatas = [(attrs['relation_id'], new_rank+1) for new_rank, (sup, sub, attrs) in enumerate(relations)]
  407. sql = insert(MySQL.relations_table_name, columns=(MySQL.relations_pkname, 'rank'), values=rdatas, on_duplicate_key_update={'rank', mosql.util.raw('VALUES(`rank`)')})
  408. with self.connection as cur:
  409. if cur.execute(sql) != 1:
  410. return False
  411. else:
  412. return True
  413. ## @brief Set the rank of a relation identified by its ID
  414. #
  415. # @note this solution is not the more efficient solution but it
  416. # garantee that ranks are continuous and starts at 1
  417. # @warning there is no way to fail on rank parameters even giving very bad parameters, if you want a method that may fail on rank use set_relation_rank() instead
  418. # @param id_relation int : relation ID
  419. # @param rank int|str : 'first', 'last', or an integer value
  420. # @throw leapi.leapi.LeObjectQueryError if id_relation don't exists
  421. def _set_relation_rank(self, id_relation, rank):
  422. ret = self.get_relation(id_relation, no_attr=True)
  423. if not ret:
  424. raise leapi.leapi.LeObjectQueryError("No relation with id_relation = %d" % id_relation)
  425. lesup = ret['lesup']
  426. lesub = ret['lesup']
  427. cur_rank = ret['rank']
  428. rank = 1 if rank == 'first' or rank < 1 else rank
  429. if cur_rank == rank:
  430. return True
  431. relations = self.get_related(lesup, lesub.__class__, get_sub=True)
  432. if not isinstance(rank, int) or rank > len(relations):
  433. rank = len(relations)
  434. if cur_rank == rank:
  435. return True
  436. #insert the relation at the good position
  437. our_relation = relations.pop(cur_rank)
  438. relations.insert(our_relation, rank)
  439. #gathering (relation_id, new_rank)
  440. rdatas = [(attrs['relation_id'], new_rank + 1) for new_rank, (sup, sub, attrs) in enumerate(relations)]
  441. sql = insert(MySQL.relations_table_name, columns=(MySQL.relations_pkname, 'rank'), values=rdatas, on_duplicate_key_update={'rank', mosql.util.raw('VALUES(`rank`)')})
  442. ## @brief Check a rank value
  443. # @param rank int | str : Can be an integer >= 1 , 'first' or 'last'
  444. # @throw ValueError if the rank is not valid
  445. def _check_rank(self, rank):
  446. if isinstance(rank, str) and rank != 'first' and rank != 'last':
  447. raise ValueError("Invalid rank value : %s" % rank)
  448. elif isinstance(rank, int) and rank < 1:
  449. raise ValueError("Invalid rank value : %d" % rank)
  450. else:
  451. raise ValueError("Invalid rank type : %s" % type(rank))
  452. ## @brief Link two object given a relation nature, depth and rank
  453. # @param lesup LeObject : a LeObject
  454. # @param lesub LeObject : a LeObject
  455. # @param nature str|None : The relation nature or None if rel2type
  456. # @param rank int : a rank
  457. def add_relation(self, lesup, lesub, nature=None, depth=None, rank=None, **rel_attr):
  458. if len(rel_attr) > 0 and nature is not None:
  459. #not a rel2type but have some relation attribute
  460. raise AttributeError("No relation attributes allowed for non rel2type relations")
  461. with self.connection as cur:
  462. sql = insert(self.datasource_utils.relations_table_name, {'id_sup': lesup.lodel_id, 'id_sub': lesub.lodel_id, 'nature': nature, 'rank': rank, 'depth': depth})
  463. if cur.execute(sql) != 1:
  464. raise RuntimeError("Unknow SQL error")
  465. if len(rel_attr) > 0:
  466. #a relation table exists
  467. cur.execute('SELECT last_insert_id()')
  468. relation_id, = cur.fetchone()
  469. raise NotImplementedError()
  470. return True
  471. ## @brief Delete a relation
  472. # @warning this method may not be efficient
  473. # @param id_relation int : The relation identifier
  474. # @return bool
  475. def del_relation(self, id_relation):
  476. with self.connection as cur:
  477. pk_where = {MySQL.relations_pkname: id_relation}
  478. if not MySQL.fk_on_delete_cascade and len(lesup._linked_types[lesub.__class__]) > 0:
  479. #Delete the row in the relation attribute table
  480. ret = self.get_relation(id_relation, no_attr=False)
  481. lesup = ret['lesup']
  482. lesub = ret['lesub']
  483. sql = delete(MySQL.relations_table_name, pk_where)
  484. if cur.execute(sql) != 1:
  485. raise RuntimeError("Unknown SQL Error")
  486. sql = delete(MySQL.relations_table_name, pk_where)
  487. if cur.execute(sql) != 1:
  488. raise RuntimeError("Unknown SQL Error")
  489. return True
  490. ## @brief Fetch a relation
  491. # @param id_relation int : The relation identifier
  492. # @param no_attr bool : If true dont fetch rel_attr
  493. # @return a dict{'id_relation':.., 'lesup':.., 'lesub':..,'rank':.., 'depth':.., #if not none#'nature':.., #if exists#'dict_attr':..>}
  494. #
  495. # @todo TESTS
  496. # TODO conserver, appelé par la méthode update_rank
  497. def get_relation(self, id_relation, no_attr=False):
  498. relation = dict()
  499. with self.connection as cur:
  500. sql = select(MySQL.relation_table_name, {MySQL.relations_pkname: id_relation})
  501. if cur.execute(sql) != 1:
  502. raise RuntimeError("Unknow SQL error")
  503. res = all_to_dicts(cur)
  504. if len(res) == 0:
  505. return False
  506. if len(res) > 1:
  507. raise RuntimeError("When selecting on primary key, get more than one result. Bailout")
  508. if res['nature'] is not None:
  509. raise ValueError("The relation with id %d is not a rel2type relation" % id_relation)
  510. leobj = leapi.lefactory.LeFactory.leobj_from_name('LeObject')
  511. lesup = leobj.uid2leobj(res['id_sup'])
  512. lesub = leobj.uid2leobj(res['id_sub'])
  513. relation['id_relation'] = res['id_relation']
  514. relation['lesup'] = lesup
  515. relation['lesub'] = lesub
  516. relation['rank'] = rank
  517. relation['depth'] = depth
  518. if res['nature'] is not None:
  519. relation['nature'] = res['nature']
  520. if not no_attr and res['nature'] is None and len(lesup._linked_types[lesub.__class__]) != 0:
  521. #Fetch relation attributes
  522. rel_attr_table = MySQL.get_r2t2table_name(lesup.__class__.__name__, lesub.__class__.__name__)
  523. sql = select(MySQL.rel_attr_table, {MySQL.relations_pkname: id_relation})
  524. if cur.execute(sql) != 1:
  525. raise RuntimeError("Unknow SQL error")
  526. res = all_to_dicts(cur)
  527. if len(res) == 0:
  528. #Here raising a warning and adding empty (or default) attributes will be better
  529. raise RuntimeError("This relation should have attributes but none found !!!")
  530. if len(res) > 1:
  531. raise RuntimeError("When selecting on primary key, get more than one result. Bailout")
  532. attrs = res[0]
  533. relation['rel_attr'] = attrs
  534. return relation
  535. ## @brief Fetch all relations concerning an object (rel2type relations)
  536. # @param leo LeType : LeType child instance
  537. # @return a list of tuple (lesup, lesub, dict_attr)
  538. def get_relations(self, leo):
  539. sql = select(self.datasource_utils.relations_table_name, where=or_(({'id_sub': leo.lodel_id}, {'id_sup': leo.lodel_id})))
  540. with self.connection as cur:
  541. results = all_to_dicts(cur.execute(sql))
  542. relations = []
  543. for result in results:
  544. id_sup = result['id_sup']
  545. id_sub = result['id_sub']
  546. del result['id_sup']
  547. del result['id_sub']
  548. rel_attr = result
  549. relations.append((id_sup, id_sub, rel_attr))
  550. return relations
  551. ## @brief Add a superior to a LeObject
  552. # @note in the MySQL version the method will have a depth=None argument to allow reccursive calls to add all the path to the root with corresponding depth
  553. # @param lesup LeType : superior LeType child class instance
  554. # @param lesub LeType : subordinate LeType child class instance
  555. # @param nature str : A relation nature @ref EditorialModel.classtypesa
  556. # @param rank int : The rank of this relation
  557. # @param depth None|int : The depth of the relation (used to make reccursive calls in order to link with all superiors)
  558. # @return The relation ID or False if fails
  559. def add_superior(self, lesup, lesub, nature, rank, depth=None):
  560. params = {'id_sup': lesup.lodel_id, 'id_sub': lesub.lodel_id, 'nature': nature, 'rank': rank}
  561. if depth is not None:
  562. params['depth'] = depth
  563. sql_insert = insert(self.datasource_utils.relations_table_name, params)
  564. with self.connection as cur:
  565. if cur.execute(sql_insert) != 1:
  566. return False
  567. cur.execute('SELECT last_insert_id()')
  568. relation_id, = cur.fetchone()
  569. if nature in EmNature.getall():
  570. parent_superiors = lesup.superiors()
  571. for superior in parent_superiors:
  572. depth = depth - 1 if depth is not None else 1
  573. self.add_relation(lesup=superior.lodel_id, lesub=lesub.lodel_id, nature=nature, depth=depth, rank=rank)
  574. return relation_id
  575. ## @brief Fetch a superiors list ordered by depth for a LeType
  576. # @param lesub LeType : subordinate LeType child class instance
  577. # @param nature str : A relation nature @ref EditorialModel.classtypes
  578. # @return A list of LeType ordered by depth (the first is the direct superior)
  579. def get_superiors(self, lesub, nature):
  580. sql = select(
  581. self.datasource_utils.relations_table_name,
  582. columns=('id_sup',),
  583. where={'id_sub': lesub.lodel_id, 'nature': nature},
  584. order_by=('depth desc',)
  585. )
  586. result = []
  587. with self.connection as cur:
  588. results = all_to_dicts(cur.execute(sql))
  589. superiors = [LeType(result['id_sup']) for result in results]
  590. return superiors
  591. ## @brief Fetch the list of the subordinates given a nature
  592. # @param lesup LeType : superior LeType child class instance
  593. # @param nature str : A relation nature @ref EditorialModel.classtypes
  594. # @return A list of LeType ordered by rank that are subordinates of lesup in a "nature" relation
  595. def get_subordinates(self, lesup, nature):
  596. with self.connection as cur:
  597. id_sup = lesup.lodel_id if isinstance(lesup, leapi.letype.LeType) else MySQL.leroot_lodel_id
  598. sql = select(
  599. MySQL.relations_table_name,
  600. columns=('id_sup',),
  601. where={'id_sup': id_sup, 'nature': nature},
  602. order_by=('rank',)
  603. )
  604. cur.execut(sql)
  605. res = all_to_dicts(cur)
  606. return [LeType(r['id_sup']) for r in res]
  607. # ================================================================================================================ #
  608. # FONCTIONS A SUPPRIMER #
  609. # ================================================================================================================ #
  610. ## @brief inserts a new object
  611. # @param letype LeType
  612. # @param leclass LeClass
  613. # @param datas dict : dictionnary of field:value pairs to save
  614. # @return int : lodel_id of the created object
  615. # @todo add the returning clause and the insertion in "object"
  616. # def insert(self, letype, leclass, datas):
  617. # if isinstance(datas, list):
  618. # res = list()
  619. # for data in datas:
  620. # res.append(self.insert(letype, leclass, data))
  621. # return res if len(res)>1 else res[0]
  622. # elif isinstance(datas, dict):
  623. #
  624. # object_datas = {'class_id': leclass._class_id, 'type_id': letype._type_id}
  625. #
  626. # cur = self.datasource_utils.query(self.connection, insert(self.datasource_utils.objects_table_name, object_datas))
  627. # lodel_id = cur.lastrowid
  628. #
  629. # datas[self.datasource_utils.field_lodel_id] = lodel_id
  630. # query_table_name = self.datasource_utils.get_table_name_from_class(leclass.__name__)
  631. # self.datasource_utils.query(self.connection, insert(query_table_name, datas))
  632. #
  633. # return lodel_id
  634. ## @brief search for a collection of objects
  635. # @param leclass LeClass
  636. # @param letype LeType
  637. # @field_list list
  638. # @param filters list : list of tuples formatted as (FIELD, OPERATOR, VALUE)
  639. # @param relation_filters list : list of tuples formatted as (('superior'|'subordinate', FIELD), OPERATOR, VALUE)
  640. # @return list
  641. # def get(self, leclass, letype, field_list, filters, relational_filters=None):
  642. #
  643. # if leclass is None:
  644. # query_table_name = self.datasource_utils.objects_table_name
  645. # else:
  646. # query_table_name = self.datasource_utils.get_table_name_from_class(leclass.__name__)
  647. # where_filters = self._prepare_filters(filters, query_table_name)
  648. # join_fields = {}
  649. #
  650. # if relational_filters is not None and len(relational_filters) > 0:
  651. # rel_filters = self._prepare_rel_filters(relational_filters)
  652. # for rel_filter in rel_filters:
  653. # # join condition
  654. # relation_table_join_field = "%s.%s" % (self.datasource_utils.relations_table_name, self.RELATIONS_POSITIONS_FIELDS[rel_filter['position']])
  655. # query_table_join_field = "%s.%s" % (query_table_name, self.datasource_utils.field_lodel_id)
  656. # join_fields[query_table_join_field] = relation_table_join_field
  657. # # Adding "where" filters
  658. # where_filters['%s.%s' % (self.datasource_utils.relations_table_name, self.datasource_utils.relations_field_nature)] = rel_filter['nature']
  659. # where_filters[rel_filter['condition_key']] = rel_filter['condition_value']
  660. #
  661. # # building the query
  662. # query = select(query_table_name, where=where_filters, select=field_list, joins=join(self.datasource_utils.relations_table_name, join_fields))
  663. # else:
  664. # query = select(query_table_name, where=where_filters, select=field_list)
  665. #
  666. # Executing the query
  667. # cur = self.datasource_utils.query(self.connection, query)
  668. # results = all_to_dicts(cur)
  669. #
  670. # return results
  671. ## @brief delete an existing object
  672. # @param letype LeType
  673. # @param leclass LeClass
  674. # @param filters list : list of tuples formatted as (FIELD, OPERATOR, VALUE)
  675. # @param relational_filters list : list of tuples formatted as (('superior'|'subordinate', FIELD), OPERATOR, VALUE)
  676. # @return bool : True on success
  677. # def delete(self, letype, leclass, filters, relational_filters):
  678. # query_table_name = self.datasource_utils.get_table_name_from_class(leclass.__name__)
  679. # prep_filters = self._prepare_filters(filters, query_table_name)
  680. # prep_rel_filters = self._prepare_rel_filters(relational_filters)
  681. #
  682. # if len(prep_rel_filters) > 0:
  683. # query = "DELETE %s FROM " % query_table_name
  684. #
  685. # for prep_rel_filter in prep_rel_filters:
  686. # query += "%s INNER JOIN %s ON (%s.%s = %s.%s)" % (
  687. # self.datasource_utils.relations_table_name,
  688. # query_table_name,
  689. # self.datasource_utils.relations_table_name,
  690. # prep_rel_filter['position'],
  691. # query_table_name,
  692. # self.datasource_utils.field_lodel_id
  693. # )
  694. #
  695. # if prep_rel_filter['condition_key'][0] is not None:
  696. # prep_filters[("%s.%s" % (self.datasource_utils.relations_table_name, prep_rel_filter['condition_key'][0]), prep_rel_filter['condition_key'][1])] = prep_rel_filter['condition_value']
  697. #
  698. # if prep_filters is not None and len(prep_filters) > 0:
  699. # query += " WHERE "
  700. # filter_counter = 0
  701. # for filter_item in prep_filters:
  702. # if filter_counter > 1:
  703. # query += " AND "
  704. # query += "%s %s %s" % (filter_item[0][0], filter_item[0][1], filter_item[1])
  705. # else:
  706. # query = delete(query_table_name, filters)
  707. #
  708. # query_delete_from_object = delete(self.datasource_utils.objects_table_name, {'lodel_id': filters['lodel_id']})
  709. # with self.connection as cur:
  710. # cur.execute(query)
  711. # cur.execute(query_delete_from_object)
  712. #
  713. # return True
  714. ## @brief update an existing object's data
  715. # @param letype LeType
  716. # @param leclass LeClass
  717. # @param filters list : list of tuples formatted as (FIELD, OPERATOR, VALUE)
  718. # @param rel_filters list : list of tuples formatted as (('superior'|'subordinate', FIELD), OPERATOR, VALUE)
  719. # @param data dict
  720. # @return bool
  721. # @todo prendre en compte les rel_filters
  722. # def update(self, letype, leclass, filters, rel_filters, data):
  723. #
  724. # query_table_name = self.datasource_utils.get_table_name_from_class(leclass.__name__)
  725. # where_filters = filters
  726. # set_data = data
  727. #
  728. # prepared_rel_filters = self._prepare_rel_filters(rel_filters)
  729. #
  730. # Building the query
  731. # query = update(table=query_table_name, where=where_filters, set=set_data)
  732. # Executing the query
  733. # with self.connection as cur:
  734. # cur.execute(query)
  735. # return True