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.

leapidatasource.py 38KB

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