PageRenderTime 114ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 1ms

/Server/bkr/server/search_utility.py

https://github.com/beaker-project/beaker
Python | 1582 lines | 1562 code | 12 blank | 8 comment | 36 complexity | 495c7b3ec0b7bc90d5c99388c142f109 MD5 | raw file
Possible License(s): GPL-2.0, CC-BY-SA-3.0

Large files files are truncated, but you can click here to view the full file

  1. # This program is free software; you can redistribute it and/or modify
  2. # it under the terms of the GNU General Public License as published by
  3. # the Free Software Foundation; either version 2 of the License, or
  4. # (at your option) any later version.
  5. import datetime
  6. import decimal
  7. import bkr.server.model as model
  8. import re
  9. import sqlalchemy
  10. from copy import copy
  11. from turbogears import flash
  12. import sqlalchemy.types
  13. from sqlalchemy import or_, and_, not_
  14. from sqlalchemy.sql import visitors, select
  15. from sqlalchemy.sql.expression import true, false
  16. from sqlalchemy.exc import InvalidRequestError
  17. from sqlalchemy.orm.exc import NoResultFound
  18. from sqlalchemy.orm import aliased, joinedload
  19. from bkr.server.database import session
  20. from bkr.server.model import Key as KeyModel
  21. from bkr.common.bexceptions import BeakerException
  22. import logging
  23. log = logging.getLogger(__name__)
  24. def get_alias_target(aliased_class):
  25. # SQLAlchemy 0.8+ has a proper inspection API,
  26. # on earlier versions we just hack it
  27. try:
  28. from sqlalchemy import inspect
  29. except ImportError:
  30. return aliased_class._AliasedClass__target
  31. return inspect(aliased_class).mapper.entity #pylint: disable=E1102
  32. def _lucene_coerce_for_column(column, term):
  33. if isinstance(column.type, sqlalchemy.types.Boolean):
  34. # Solr stores boolean fields as "true"/"false" so let's follow that
  35. if term.lower() == 'true':
  36. return True
  37. elif term.lower() == 'false':
  38. return False
  39. else:
  40. raise ValueError()
  41. if isinstance(column.type, sqlalchemy.types.Integer):
  42. return int(term)
  43. elif isinstance(column.type, sqlalchemy.types.Numeric):
  44. try:
  45. return decimal.Decimal(term)
  46. except decimal.InvalidOperation:
  47. raise ValueError()
  48. elif isinstance(column.type, sqlalchemy.types.DateTime):
  49. return datetime.datetime.strptime(term, '%Y-%m-%d')
  50. else: # treat everything else as a string
  51. return term
  52. class _LuceneTermQuery(object):
  53. def __init__(self, text):
  54. self.text = text
  55. def apply(self, column):
  56. if self.text == u'*':
  57. return column != None
  58. try:
  59. value = _lucene_coerce_for_column(column, self.text)
  60. except ValueError:
  61. return false()
  62. if isinstance(column.type, sqlalchemy.types.DateTime):
  63. # date searches are implicitly a range across one day
  64. return and_(column >= value,
  65. column <= value.replace(hour=23, minute=59, second=59))
  66. if isinstance(column.type, sqlalchemy.types.String) and '*' in value:
  67. return column.like(value.replace('*', '%'))
  68. return column == value
  69. class _LuceneRangeQuery(object):
  70. def __init__(self, start, end, start_inclusive, end_inclusive):
  71. self.start = start
  72. self.end = end
  73. self.start_inclusive = start_inclusive
  74. self.end_inclusive = end_inclusive
  75. def apply(self, column):
  76. if self.start == u'*':
  77. start_clause = true()
  78. else:
  79. try:
  80. start_value = _lucene_coerce_for_column(column, self.start)
  81. except ValueError:
  82. start_clause = false()
  83. else:
  84. if self.start_inclusive:
  85. start_clause = column >= start_value
  86. else:
  87. start_clause = column > start_value
  88. if self.end == u'*':
  89. end_clause = true()
  90. else:
  91. try:
  92. end_value = _lucene_coerce_for_column(column, self.end)
  93. except ValueError:
  94. end_clause = false()
  95. else:
  96. if isinstance(end_value, datetime.datetime):
  97. end_value = end_value.replace(hour=23, minute=59, second=59)
  98. if self.end_inclusive:
  99. end_clause = column <= end_value
  100. else:
  101. end_clause = column < end_value
  102. return and_(start_clause, end_clause)
  103. def _apply_lucene_query(lucene_query, chain):
  104. if not isinstance(chain, tuple):
  105. return lucene_query.apply(chain)
  106. if len(chain) == 1:
  107. return lucene_query.apply(chain[0])
  108. else:
  109. return chain[0].any(_apply_lucene_query(lucene_query, chain[1:]))
  110. _lucene_query_pattern = re.compile(r"""
  111. (?P<negation>-)?
  112. (?:(?P<field>[^'"\s]+):)?
  113. (?:['"](?P<quoted_term>[^'"]*)['"]
  114. | (?P<range_term>[\[{] \s* (?P<range_start>[^\]}]*) \s+ TO \s+ (?P<range_end>[^\]}]*) \s* [\]}])
  115. | (?P<malformed_range>[\[{] [^\]}]* [\]}])
  116. | (?P<term>\S+)
  117. )""",
  118. re.VERBOSE)
  119. def lucene_to_sqlalchemy(querystring, search_columns, default_columns):
  120. """
  121. Parses the given *querystring* using a Lucene-like syntax and converts it
  122. to a SQLAlchemy filter clause.
  123. http://lucene.apache.org/core/3_6_0/queryparsersyntax.html
  124. The *search_columns* parameter is a dict of field names from the query
  125. string mapped to column definitions. A column definition can be either:
  126. * A regular SQLAlchemy column or column property (or any other
  127. compatible construct). For example, User.user_name. This is used for
  128. columns which are selected directly by the query or in a one-to-one
  129. join which is applied to the query, for example
  130. System.query.join(System.user).
  131. * A tuple making a chain of relationships ending in a single column:
  132. (relationship_1, ..., relationship_N, column)
  133. For example, (Group.users, User.user_name). This is used for
  134. one-to-many relationships which must be filtered using .any() to
  135. produce an EXISTS clause.
  136. The *default_columns* parameter is a list of column definitions (as above)
  137. which will be used for matching terms in the query string which don't have
  138. an explicit field name.
  139. Maybe one day we will be using Lucene/Solr for real...
  140. """
  141. # This only understands the subset of Lucene syntax used by the search
  142. # bar. In particular, grouping using parentheses is not supported.
  143. # We should probably use a proper parser instead of a hacky regexp.
  144. clauses = []
  145. for match in _lucene_query_pattern.finditer(querystring):
  146. if match.group('range_term') is not None:
  147. start = match.group('range_start')
  148. end = match.group('range_end')
  149. start_inclusive = match.group('range_term').startswith('[')
  150. end_inclusive = match.group('range_term').endswith(']')
  151. lucene_query = _LuceneRangeQuery(start, end,
  152. start_inclusive, end_inclusive)
  153. elif match.group('malformed_range') is not None:
  154. lucene_query = _LuceneTermQuery(match.group('malformed_range'))
  155. elif match.group('quoted_term') is not None:
  156. lucene_query = _LuceneTermQuery(match.group('quoted_term'))
  157. else:
  158. lucene_query = _LuceneTermQuery(match.group('term'))
  159. if match.group('field') is None:
  160. alternatives = []
  161. for column in default_columns:
  162. alternatives.append(_apply_lucene_query(lucene_query, column))
  163. clause = or_(*alternatives)
  164. elif match.group('field') in search_columns:
  165. column = search_columns[match.group('field')]
  166. clause = _apply_lucene_query(lucene_query, column)
  167. else:
  168. clause = false()
  169. if match.group('negation'):
  170. clause = not_(clause)
  171. clauses.append(clause)
  172. return and_(*clauses)
  173. class MyColumn(object):
  174. """
  175. MyColumn is a class to hold information about a mapped column,
  176. such as the actual mapped column, the type, and a relation it may
  177. have to another mapped object
  178. It should be overridden by classes that will consistently use a
  179. relation to another mapped object.
  180. """
  181. def __init__(self,relations=None, col_type=None,
  182. column_name=None, column=None, eagerload=True, aliased=True,
  183. hidden=False, **kw):
  184. if type(col_type) != type(''):
  185. raise TypeError('col_type var passed to %s must be string' % self.__class__.__name__)
  186. self._column = column
  187. self._type = col_type
  188. self._relations = relations
  189. self._column_name = column_name
  190. self._eagerload = eagerload
  191. self._aliased = aliased
  192. # Hidden columns are for backwards compatibility, we honour
  193. # searches using them but we don't offer the column in the UI.
  194. self.hidden = hidden
  195. def column_join(self, query):
  196. return_query = None
  197. onclause = getattr(self, 'onclause', None)
  198. if onclause and self.relations:
  199. relation = self.relations.pop()
  200. if self.eagerload:
  201. query = query.options(joinedload(onclause))
  202. return_query = query.outerjoin((relation, onclause))
  203. elif self.relations:
  204. if isinstance(self.relations, list):
  205. if self.eagerload:
  206. query = query.options(joinedload(*self.relations))
  207. return_query = query.outerjoin(*self.relations)
  208. else:
  209. if self.eagerload:
  210. query = query.options(joinedload(self.relations))
  211. return_query = query.outerjoin(self.relations)
  212. else:
  213. raise ValueError('Cannot join with nothing to join on')
  214. return return_query
  215. @property
  216. def column_name(self):
  217. return self._column_name
  218. @property
  219. def eagerload(self):
  220. return self._eagerload
  221. @property
  222. def type(self):
  223. return self._type
  224. @property
  225. def relations(self):
  226. return self._relations
  227. @property
  228. def column(self):
  229. return self._column
  230. @property
  231. def aliased(self):
  232. return self._aliased
  233. @property
  234. def parent_entity(self):
  235. try:
  236. return self.column.parent.entity
  237. except AttributeError: # sqlalchemy < 0.8
  238. return self.column.parententity
  239. class CpuColumn(MyColumn):
  240. """
  241. CpuColumn defines a relationship to system
  242. """
  243. def __init__(self,**kw):
  244. if not kw.has_key('relations'):
  245. kw['relations'] = 'cpu'
  246. super(CpuColumn,self).__init__(**kw)
  247. class DeviceColumn(MyColumn):
  248. """
  249. DeviceColumn defines a relationship to system
  250. """
  251. def __init__(self,**kw):
  252. if not kw.has_key('relations'):
  253. kw['relations'] = 'devices'
  254. super(DeviceColumn,self).__init__(**kw)
  255. class DiskColumn(MyColumn):
  256. def __init__(self, **kwargs):
  257. kwargs.setdefault('relations', ['disks'])
  258. super(DiskColumn, self).__init__(**kwargs)
  259. class AliasedColumn(MyColumn):
  260. def __init__(self, **kw):
  261. if 'column_name' not in kw:
  262. raise ValueError('an AliasedColumn must have a column_name')
  263. relations = kw['relations']
  264. target_table = kw.get('target_table')
  265. if not callable(relations): # We can check it's contents
  266. if len(relations) > 1 and not target_table:
  267. raise ValueError('Multiple relations specified, '
  268. + 'which one to filter on has not been specified')
  269. if len(set(relations)) != len(relations):
  270. raise ValueError('There can be no duplicates in table')
  271. self.target_table = target_table
  272. onclause = kw.get('onclause')
  273. if onclause and len(relations) > 1:
  274. raise ValueError('You can only pass one relation if using an onclause')
  275. self.onclause = onclause
  276. super(AliasedColumn, self).__init__(**kw)
  277. @property
  278. def relations(self):
  279. return self._relations
  280. @property
  281. def column(self):
  282. return self._column
  283. @relations.setter
  284. def relations(self, val):
  285. self._relations = val
  286. @column.setter
  287. def column(self, val):
  288. self._column = val
  289. class KeyColumn(AliasedColumn):
  290. def __init__(self, **kw):
  291. kw['relations'].append(model.Key)
  292. kw['column_name'] = 'key_value'
  293. super(KeyColumn, self).__init__(**kw)
  294. class KeyStringColumn(KeyColumn):
  295. def __init__(self, **kw):
  296. kw['target_table'] = model.Key_Value_String
  297. kw['relations'] = [model.Key_Value_String]
  298. kw['col_type'] = 'string'
  299. super(KeyStringColumn, self).__init__(**kw)
  300. def column_join(self, query):
  301. """
  302. column_join is used here to specify the oncaluse for the
  303. KeyValueString to Key join.
  304. """
  305. relations = self.relations
  306. for relation in relations:
  307. if get_alias_target(relation) == model.Key:
  308. key_ = relation
  309. if get_alias_target(relation) == model.Key_Value_String:
  310. key_string = relation
  311. return query.outerjoin((key_string, key_string.system_id==model.System.id),
  312. (key_, key_.id==key_string.key_id))
  313. class KeyIntColumn(KeyColumn):
  314. def __init__(self, **kw):
  315. kw['target_table'] = model.Key_Value_Int
  316. kw['relations'] = [model.Key_Value_Int]
  317. kw['col_type'] = 'int'
  318. super(KeyIntColumn, self).__init__(**kw)
  319. def column_join(self, query):
  320. """
  321. column_join is used here to specify the oncaluse for the
  322. KeyValueString to Key join.
  323. """
  324. relations = self.relations
  325. for relation in relations:
  326. if get_alias_target(relation) == model.Key:
  327. key_ = relation
  328. if get_alias_target(relation) == model.Key_Value_Int:
  329. key_int = relation
  330. return query.outerjoin((key_int, key_int.system_id==model.System.id),
  331. (key_, key_.id==key_int.key_id))
  332. class KeyCreator(object):
  333. """
  334. KeyCreator takes care of determining what key column needs
  335. to be created.
  336. """
  337. INT = 0
  338. STRING = 1
  339. @classmethod
  340. def create(cls, type, **kw):
  341. if type is cls.STRING:
  342. obj = KeyStringColumn(**kw)
  343. elif type is cls.INT:
  344. obj = KeyIntColumn(**kw)
  345. else:
  346. raise ValueError('Key type needs to be either INT or STRING')
  347. return obj
  348. class Modeller(object):
  349. """
  350. Modeller class provides methods relating to different datatypes and
  351. operations available to those datatypes
  352. """
  353. def __init__(self):
  354. self.structure = {'string' : {'is' : lambda x,y: self.equals(x,y),
  355. 'is not' : lambda x,y: self.not_equal(x,y),
  356. 'contains' : lambda x,y: self.contains(x,y), },
  357. 'text' : {'is' : lambda x,y: self.equals(x,y),
  358. 'is not' : lambda x,y: self.not_equal(x,y),
  359. 'contains' : lambda x,y: self.contains(x,y), },
  360. 'integer' : {'is' : lambda x,y: self.equals(x,y),
  361. 'is not' : lambda x,y: self.not_equal(x,y),
  362. 'less than' : lambda x,y: self.less_than(x,y),
  363. 'greater than' : lambda x,y: self.greater_than(x,y), },
  364. 'unicode' : {'is' : lambda x,y: self.equals(x,y),
  365. 'is not' : lambda x,y: self.not_equal(x,y),
  366. 'contains' : lambda x,y: self.contains(x,y), },
  367. 'boolean' : {'is' : lambda x,y: self.bool_equals(x,y),
  368. 'is not' : lambda x,y: self.bool_not_equal(x,y), },
  369. 'date' : {'is' : lambda x,y: self.equals(x,y),
  370. 'after' : lambda x,y: self.greater_than(x,y),
  371. 'before' : lambda x,y: self.less_than(x,y),},
  372. 'generic' : {'is' : lambda x,y: self.equals(x,y) ,
  373. 'is not': lambda x,y: self.not_equal(x,y), },
  374. }
  375. def less_than(self,x,y):
  376. return x < y
  377. def greater_than(self,x,y):
  378. return x > y
  379. def bool_not_equal(self,x,y):
  380. bool_y = int(y)
  381. return x != bool_y
  382. def bool_equals(self,x,y):
  383. bool_y = int(y)
  384. return x == bool_y
  385. def not_equal(self,x,y):
  386. wildcard_y = re.sub('\*','%',y)
  387. if wildcard_y != y: #looks like we found a wildcard
  388. return not_(x.like(wildcard_y))
  389. if not y:
  390. return and_(x != None,x != y)
  391. return or_(x != y, x == None)
  392. def equals(self,x,y):
  393. wildcard_y = re.sub('\*','%',y)
  394. if wildcard_y != y: #looks like we found a wildcard
  395. return x.like(wildcard_y)
  396. if not y:
  397. return or_(x == None,x==y)
  398. return x == y
  399. def contains(self,x,y):
  400. return x.like('%%%s%%' % y )
  401. def return_function(self,type,operator,loose_match=True):
  402. """
  403. return_function will return the particular python function to be applied to a type/operator combination
  404. (i.e sqlalchemy.types.Integer and 'greater than')
  405. """
  406. try:
  407. op_dict = self.structure[type]
  408. except KeyError, (error):
  409. if loose_match:
  410. op_dict = self.loose_type_match(type)
  411. if not op_dict:
  412. op_dict = self.structure['generic']
  413. try:
  414. return op_dict[operator]
  415. except KeyError,e:
  416. flash(_('%s is not a valid operator' % operator))
  417. raise
  418. def return_operators(self,field_type,loose_match=True):
  419. # loose_match flag will specify if we should try and 'guess' what data type it is
  420. # int,integer,num,number,numeric will match for an integer.
  421. # string, word match for sqlalchemy.types.string
  422. # bool,boolean will match for sqlalchemy.types.boolean
  423. operators = None
  424. field_type = field_type.lower()
  425. try:
  426. operators = self.structure[field_type]
  427. except KeyError, (error):
  428. if loose_match:
  429. operators = self.loose_type_match(field_type)
  430. if operators is None:
  431. operators = self.structure['generic']
  432. return operators.keys()
  433. def loose_type_match(self,field_type):
  434. type_lower = field_type.lower()
  435. int_pattern = '^int(?:eger)?$|^num(?:ber|eric)?$'
  436. string_pattern = '^string$|^word$'
  437. bool_pattern = '^bool(?:ean)?$'
  438. operators = None
  439. if re.match(int_pattern,type_lower):
  440. operators = self.structure['integer']
  441. elif re.match(string_pattern,type_lower):
  442. operators = self.structure['string']
  443. elif re.match(bool_pattern,type_lower):
  444. operators = self.structure['boolean']
  445. return operators
  446. class Search(object):
  447. def __init__(self, query):
  448. self.queri = query
  449. @classmethod
  450. def get_search_options_worker(cls,search,col_type):
  451. return_dict = {}
  452. #Determine what field type we are dealing with. If it is Boolean, convert our values to 0 for False
  453. # and 1 for True
  454. if col_type.lower() == 'boolean':
  455. search['values'] = { 0:'False', 1:'True'}
  456. #Determine if we have search values. If we do, then we should only have the operators
  457. # 'is' and 'is not'.
  458. if search['values']:
  459. search['operators'] = filter(lambda x: x == 'is' or x == 'is not', search['operators'])
  460. search['operators'].sort()
  461. return_dict['search_by'] = search['operators']
  462. return_dict['search_vals'] = search['values']
  463. return return_dict
  464. @classmethod
  465. def get_search_options(cls, table_field, *args, **kw):
  466. field = table_field
  467. search = cls.search_on(field)
  468. col_type = cls.field_type(field)
  469. return cls.get_search_options_worker(search,col_type)
  470. def append_results(self,value,column,operation,**kw):
  471. value = value.strip()
  472. pre = self.pre_operations(column,operation,value,**kw)
  473. cls_name = re.sub('Search','',self.__class__.__name__)
  474. cls = globals()[cls_name]
  475. try:
  476. mycolumn = cls.searchable_columns[column]
  477. except KeyError,e:
  478. flash(_(u'%s is not a valid search criteria' % column))
  479. raise
  480. self.do_joins(mycolumn)
  481. try:
  482. if pre['col_op_filter']:
  483. filter_func = pre['col_op_filter']
  484. filter_final = lambda: filter_func(mycolumn.column,value)
  485. else:
  486. filter_final = self.return_standard_filter(mycolumn,operation,value)
  487. except KeyError,e:
  488. log.error(e)
  489. return self.queri
  490. except AttributeError,e:
  491. log.error(e)
  492. return self.queri
  493. self.queri = self.queri.filter(filter_final())
  494. def return_results(self):
  495. return self.queri
  496. def do_joins(self,mycolumn):
  497. if mycolumn.relations:
  498. try:
  499. #This column has specified it needs a join, so let's add it to the all the joins
  500. #that are pertinent to this class. We do this so there is only one point where we add joins
  501. #to the query
  502. #cls_ref.joins.add_join(col_string = str(column),join=mycolumn.join)
  503. relations = mycolumn.relations
  504. aliased = mycolumn.aliased
  505. if not isinstance(relations, list):
  506. self.queri = self.queri.outerjoin(relations, aliased=aliased)
  507. else:
  508. for relation in relations:
  509. if isinstance(relation, list):
  510. self.queri = self.queri.outerjoin(*relation, aliased=aliased)
  511. else:
  512. self.queri = self.queri.outerjoin(*relations, aliased=aliased)
  513. break
  514. except TypeError, (error):
  515. log.error('Column %s has not specified joins validly:%s' % (mycolumn, error))
  516. def return_standard_filter(self,mycolumn,operation,value,loose_match=True):
  517. col_type = mycolumn.type
  518. modeller = Modeller()
  519. filter_func = modeller.return_function(col_type,operation,loose_match=True)
  520. return lambda: filter_func(mycolumn.column,value)
  521. def pre_operations(self,column,operation,value,cls_ref=None,**kw):
  522. #First let's see if we have a column X operation specific filter
  523. #We will only need to use these filter by table column if the ones from Modeller are
  524. #inadequate.
  525. #If we are looking at the System class and column 'arch' with the 'is not' operation, it will try and get
  526. # System.arch_is_not_filter
  527. if cls_ref is None:
  528. match_obj = re.search('^(.+)?Search$',self.__class__.__name__)
  529. cls_ref = globals()[match_obj.group(1)]
  530. if cls_ref is None:
  531. raise BeakerException('No cls_ref passed in and class naming convention did give valid class')
  532. results_dict = {}
  533. underscored_operation = re.sub(' ','_',operation)
  534. column_match = re.match('^(.+)?/(.+)?$',column)
  535. try:
  536. if column_match.group():
  537. #if We are searchong on a searchable_column that is something like 'System/Name'
  538. #and the 'is not' operation, it will look for the SystemName_is_not_filter method
  539. column_mod = '%s%s' % (column_match.group(1).capitalize(),column_match.group(2).capitalize())
  540. except AttributeError, (error):
  541. column_mod = column.lower()
  542. col_op_filter = getattr(cls_ref,'%s_%s_filter' % (column_mod,underscored_operation),None)
  543. results_dict.update({'col_op_filter':col_op_filter})
  544. #At this point we can also call a custom function before we try to append our results
  545. col_op_pre = getattr(cls_ref,'%s_%s_pre' % (column_mod,underscored_operation),None)
  546. if col_op_pre is not None:
  547. results_dict.update({'results_from_pre': col_op_pre(value,col=column,op = operation, **kw)})
  548. return results_dict
  549. @classmethod
  550. def search_on(cls,field,cls_ref=None):
  551. """
  552. search_on() takes the field name we will search by, and
  553. returns the operations suitable for the field type it represents
  554. """
  555. if cls_ref is None:
  556. match_obj = re.search('^(.+)?Search$',cls.__name__)
  557. cls_ref = globals()[match_obj.group(1)]
  558. if cls_ref is None:
  559. raise BeakerException('No cls_ref passed in and class naming convention did give valid class')
  560. try:
  561. field_type = cls_ref.get_field_type(field)
  562. vals = None
  563. try:
  564. vals = cls_ref.search_values(field)
  565. except AttributeError:
  566. log.debug('Not using predefined search values for %s->%s' % (cls_ref.__name__,field))
  567. except AttributeError, (error):
  568. log.error('Error accessing attribute within search_on: %s' % (error))
  569. else:
  570. return dict(operators = cls_ref.search_operators(field_type), values=vals)
  571. @classmethod
  572. def field_type(cls,field):
  573. """
  574. Takes a field string (ie'Processor') and returns the type of the field
  575. """
  576. match_obj = re.search('^(.+)?Search$',cls.__name__)
  577. cls_ref = globals()[match_obj.group(1)]
  578. field_type = cls_ref.get_field_type(field)
  579. if field_type:
  580. return field_type
  581. else:
  582. log.debug('There was a problem in retrieving the field_type for the column %s' % field)
  583. return
  584. @classmethod
  585. def translate_class_to_name(cls, class_ref):
  586. """Translate the class ref into the text that is used in the 'Table' column"""
  587. if hasattr(class_ref, 'display_name'):
  588. return class_ref.display_name
  589. else:
  590. return class_ref.__name__
  591. @classmethod
  592. def translate_name_to_class(cls, display_name):
  593. """Translate the text in the 'Table' column to the internal class"""
  594. class_ref = None
  595. # First check if any SystemObject has a 'display_name'
  596. # attribute that matches, otherwise check against the class
  597. # name
  598. for system_object_class in SystemObject.__subclasses__():
  599. if hasattr(system_object_class, 'display_name') and \
  600. system_object_class.display_name == display_name:
  601. class_ref = system_object_class
  602. break
  603. if class_ref is None:
  604. for system_object_class in SystemObject.__subclasses__():
  605. if system_object_class.__name__ == display_name:
  606. class_ref = system_object_class
  607. break
  608. return class_ref
  609. @classmethod
  610. def split_class_field(cls,class_field):
  611. class_field_list = class_field.split('/')
  612. display_name = class_field_list[0]
  613. field = class_field_list[1]
  614. return (display_name,field)
  615. @classmethod
  616. def create_search_table(cls,*args,**kw):
  617. cls_name = re.sub('Search','',cls.__name__)
  618. cls_ref = globals()[cls_name]
  619. searchable = cls_ref.get_searchable(*args,**kw)
  620. searchable.sort()
  621. return searchable
  622. @classmethod
  623. def create_complete_search_table(cls, *args, **kw):
  624. searchable = cls.create_search_table(*args, **kw)
  625. table_options = {}
  626. for col in searchable:
  627. table_options[col] = cls.get_search_options(col)
  628. return table_options
  629. class RecipeSearch(Search): pass
  630. class JobSearch(Search):
  631. search_table = []
  632. class TaskSearch(Search):
  633. search_table = []
  634. class DistroSearch(Search):
  635. search_table = []
  636. class DistroTreeSearch(Search): pass
  637. class KeySearch(Search):
  638. search_table = []
  639. @classmethod
  640. def get_search_options(cls, keyvalue_field, *args, **kw):
  641. return_dict = {}
  642. search = System.search.search_on_keyvalue(keyvalue_field)
  643. search.sort()
  644. return_dict['search_by'] = search
  645. return return_dict
  646. class LabControllerActivitySearch(Search):
  647. search_table = []
  648. class GroupActivitySearch(Search):
  649. search_table = []
  650. def __init__(self, activity):
  651. self.queri = activity
  652. class SystemActivitySearch(Search):
  653. search_table = []
  654. def __init__(self, activity):
  655. self.queri = activity
  656. class DistroTreeActivitySearch(Search):
  657. search_table = []
  658. class DistroActivitySearch(Search):
  659. search_table = []
  660. class ActivitySearch(Search):
  661. search_table = []
  662. class HistorySearch(Search):
  663. search_table = []
  664. class SystemSearch(Search):
  665. search_table = []
  666. column_table = []
  667. def __init__(self,systems=None):
  668. if systems:
  669. self.queri = systems.distinct()
  670. else:
  671. self.queri = session.query(model.System).distinct()
  672. self.system_columns_desc = []
  673. self.extra_columns_desc = []
  674. def __getitem__(self,key):
  675. pass
  676. def get_column_descriptions(self):
  677. return self.system_columns_desc
  678. def append_results(self,cls_ref,value,column,operation,**kw):
  679. """
  680. append_results() will take a value, column and operation from the search field,
  681. as well as the class of which the search pertains to, and will append the join
  682. and the filter needed to return the correct results.
  683. """
  684. #First let's see if we have a column X operation specific filter
  685. #We will only need to use these filter by table column if the ones from Modeller are
  686. #inadequate.
  687. #If we are looking at the System class and column 'arch' with the 'is not' operation, it will try and get
  688. # System.arch_is_not_filter
  689. value = value.strip()
  690. underscored_operation = re.sub(' ','_',operation)
  691. col_op_filter = getattr(cls_ref,'%s_%s_filter' % (column.lower(),underscored_operation),None)
  692. #At this point we can also call a custom function before we try to append our results
  693. col_op_pre = getattr(cls_ref,'%s_%s_pre' % (column.lower(),underscored_operation),None)
  694. if col_op_pre is not None:
  695. results_from_pre = col_op_pre(value,col=column,op = operation, **kw)
  696. else:
  697. results_from_pre = None
  698. mycolumn = cls_ref.create_column(column, results_from_pre)
  699. self.__do_join(cls_ref, mycolumn)
  700. modeller = Modeller()
  701. if col_op_filter:
  702. filter_func = col_op_filter
  703. filter_final = lambda: filter_func(mycolumn.column,value)
  704. #If you want to pass custom args to your custom filter, here is where you do it
  705. if kw.get('keyvalue'):
  706. filter_final = lambda: filter_func(mycolumn, value, key_name = kw['keyvalue'])
  707. else:
  708. #using just the regular filter operations from Modeller
  709. try:
  710. col_type = mycolumn.type
  711. except AttributeError, (error):
  712. log.error('Error accessing attribute type within append_results: %s' % (error))
  713. modeller = Modeller()
  714. filter_func = modeller.return_function(col_type,operation,loose_match=True)
  715. filter_final = lambda: filter_func(mycolumn.column,value)
  716. self.queri = self.queri.filter(filter_final())
  717. def _get_mapped_objects(self, tables):
  718. mapped_objects = []
  719. for table in tables:
  720. try:
  721. am_mapped_object = issubclass(table, model.MappedObject)
  722. if am_mapped_object:
  723. mapped_objects.append(table)
  724. except TypeError: #We are possible a relation, which is an object
  725. pass
  726. return mapped_objects
  727. def __do_join(self, cls_ref, mycolumn):
  728. if mycolumn.relations:
  729. # This column has specified it needs a join, so let's add it to the all the joins
  730. # that are pertinent to this class. We do this so there is only one point where we add joins
  731. # to the query.
  732. # Sometimes the relations/table are backrefs which are not
  733. # populated until the sqla model initialisation code
  734. if callable(mycolumn.relations):
  735. mycolumn.relations = mycolumn.relations()
  736. onclause = getattr(mycolumn, 'onclause', None)
  737. if onclause is not None and callable(mycolumn.onclause):
  738. mycolumn.onclause = mycolumn.onclause()
  739. if isinstance(mycolumn, AliasedColumn):
  740. relations = mycolumn.relations
  741. # Make sure not to deal with Collections as we can't alias those
  742. tables_to_alias = self._get_mapped_objects(relations)
  743. aliased_table = map(lambda x: aliased(x), tables_to_alias)
  744. if len(aliased_table) > 1:
  745. for at in aliased_table:
  746. if get_alias_target(at) == mycolumn.target_table:
  747. aliased_table_target = at
  748. else:
  749. aliased_table_target = aliased_table[0]
  750. mycolumn.column = getattr(aliased_table_target, mycolumn.column_name)
  751. relations = set(tables_to_alias) ^ set(relations) # Get the difference
  752. # Recombine what was aliased and not
  753. if relations:
  754. relations = list(relations) + aliased_table
  755. else:
  756. relations = aliased_table
  757. mycolumn.relations = relations
  758. else:
  759. pass
  760. self.queri = mycolumn.column_join(self.queri)
  761. def add_columns_desc(self,result_columns):
  762. if result_columns is not None:
  763. for elem in result_columns:
  764. (display_name,col) = self.split_class_field(elem)
  765. cls_ref = self.translate_name_to_class(display_name)
  766. mycolumn = cls_ref.create_column(col)
  767. # If they are System columns we won't need to explicitly add
  768. # them to the query, as they are already returned in
  769. # the System query.
  770. self.system_columns_desc.append(elem)
  771. self.__do_join(cls_ref, mycolumn)
  772. def return_results(self):
  773. return self.queri
  774. @classmethod
  775. def create_complete_search_table(cls, *args, **kw):
  776. searchable = cls.create_search_table(*args, **kw)
  777. table_options = {}
  778. for col in searchable:
  779. #if you have any custom columns (i.e Key/Value, then get their results here)
  780. if col.lower() == 'key/value':
  781. table_options[col] = {'keyvals': KeyModel.get_all_keys()}
  782. expanded_keyvals = {}
  783. for k in table_options[col]['keyvals']:
  784. expanded_keyvals.update({ k : Key.search.get_search_options(k) } )
  785. #table_options[col]['keyvals'] = { k:Key.search.get_search_options(k) }
  786. table_options[col].update(expanded_keyvals)
  787. continue
  788. table_options[col] = cls.get_search_options(col)
  789. return table_options
  790. @classmethod
  791. def create_column_table(cls,options):
  792. return cls._create_table(options,cls.column_table)
  793. @classmethod
  794. def create_search_table(cls,options):
  795. return cls._create_table(options,cls.search_table)
  796. @classmethod
  797. def _create_table(cls,options,lookup_table):
  798. """
  799. create_table will set and return the class' attribute with
  800. a list of searchable 'combinations'.
  801. These 'combinations' merely represent a table and a column.
  802. An example of a table entry may be 'CPU/Vendor' or 'System/Name'
  803. """
  804. #Clear the table if it's already been created
  805. if lookup_table != None:
  806. lookup_table = []
  807. for i in options:
  808. for class_ref ,v in i.iteritems():
  809. display_name = cls.translate_class_to_name(class_ref)
  810. for rule,v1 in v.iteritems():
  811. searchable = class_ref.get_searchable()
  812. if rule == 'all':
  813. for item in searchable:
  814. lookup_table.append('%s/%s' % (display_name,item))
  815. if rule == 'exclude':
  816. for item in searchable:
  817. if v1.count(item) < 1:
  818. lookup_table.append('%s/%s' % (display_name,item))
  819. if rule == 'include':
  820. for item in searchable:
  821. if v1.count(item) > 1:
  822. lookup_table.append('%s/%s' % (display_name,item))
  823. lookup_table.sort()
  824. return lookup_table
  825. @classmethod
  826. def field_type(cls,class_field):
  827. """
  828. Takes a class/field string (ie'CPU/Processor') and returns the type of the field
  829. """
  830. returned_class_field = cls.split_class_field(class_field)
  831. display_name = returned_class_field[0]
  832. field = returned_class_field[1]
  833. class_ref = cls.translate_name_to_class(display_name)
  834. field_type = class_ref.get_field_type(field)
  835. if field_type:
  836. return field_type
  837. else:
  838. log.debug('There was a problem in retrieving the field_type for the column %s' % field)
  839. return
  840. @classmethod
  841. def search_on_keyvalue(cls,key_name):
  842. """
  843. search_on_keyvalue() takes a key_name and returns the operations suitable for the
  844. field type it represents
  845. """
  846. row = model.Key.by_name(key_name)
  847. if row.numeric == 1:
  848. field_type = 'Numeric'
  849. elif row.numeric == 0:
  850. field_type = 'String'
  851. else:
  852. log.error('Cannot determine type for %s, defaulting to generic' % key_name)
  853. field_type = 'generic'
  854. return Key.search_operators(field_type,loose_match=True)
  855. @classmethod
  856. def search_on(cls,class_field):
  857. """
  858. search_on() takes a combination of class name and field name (i.e 'Cpu/vendor') and
  859. returns the operations suitable for the field type it represents
  860. """
  861. returned_class_field = cls.split_class_field(class_field)
  862. display_name = returned_class_field[0]
  863. field = returned_class_field[1]
  864. class_ref = cls.translate_name_to_class(display_name)
  865. try:
  866. field_type = class_ref.get_field_type(field)
  867. vals = None
  868. try:
  869. vals = class_ref.search_values(field)
  870. except AttributeError:
  871. log.debug('Not using predefined search values for %s->%s' % (class_ref.__name__,field))
  872. except AttributeError, (error):
  873. log.error('Error accessing attribute within search_on: %s' % (error))
  874. else:
  875. return dict(operators = class_ref.search_operators(field_type), values = vals)
  876. class SystemObject(object):
  877. # Defined on subclasses
  878. searchable_columns = {}
  879. search_values_dict = {}
  880. @classmethod
  881. def create_column(cls, column, *args, **kw):
  882. column = cls.searchable_columns.get(column)
  883. # AliasedColumn will be modified, so make sure
  884. # we do not pass the same object around.
  885. if isinstance(column, AliasedColumn):
  886. column = copy(column)
  887. return column
  888. @classmethod
  889. def get_field_type(cls,field):
  890. mycolumn = cls.searchable_columns.get(field)
  891. if mycolumn:
  892. try:
  893. field_type = mycolumn.type
  894. return field_type
  895. except AttributeError, (error):
  896. log.error('No type specified for %s in searchable_columns:%s' % (field,error))
  897. else:
  898. log.error('Column %s is not a mapped column nor is it part of searchable_columns' % field)
  899. @classmethod
  900. def search_values(cls,col):
  901. if cls.search_values_dict.has_key(col):
  902. return cls.search_values_dict[col]()
  903. @classmethod
  904. def get_searchable(cls,*args,**kw):
  905. """
  906. get_searchable will return the description of how the calling class can be searched by returning a list
  907. of fields that can be searched after any field filtering has
  908. been applied
  909. """
  910. searchable_columns = [k for (k,v) in cls.searchable_columns.iteritems()
  911. if v is None or not v.hidden]
  912. if 'exclude' in kw:
  913. if type(kw['without']) == type(()):
  914. for i in kw['exclude']:
  915. try:
  916. del searchable_columns[i]
  917. except KeyError,e:
  918. log.error('Cannot remove column %s from searchable column in class %s as it is not a searchable column in the first place' % (i,cls.__name__))
  919. return searchable_columns
  920. @classmethod
  921. def search_operators(cls,field):
  922. """
  923. search_operators returns a list of the different types of searches that can be done on a given field.
  924. It relies on the Modeller class, which stores these relationships.
  925. """
  926. m = Modeller()
  927. try:
  928. return m.return_operators(field)
  929. except KeyError, (e):
  930. log.error('Failed to find operators for field %s, got error: %s' % (field,e))
  931. class System(SystemObject):
  932. search = SystemSearch
  933. search_table = []
  934. searchable_columns = {'Vendor' : MyColumn(column=model.System.vendor,col_type='string'),
  935. 'Name' : MyColumn(column=model.System.fqdn,col_type='string'),
  936. 'Lender' : MyColumn(column=model.System.lender,col_type='string'),
  937. 'Location' : MyColumn(column=model.System.location, col_type='string'),
  938. 'Added' : MyColumn(column=model.System.date_added, col_type='date'),
  939. 'LastInventoried': MyColumn(column=model.System.date_lastcheckin,
  940. col_type='date'),
  941. 'Model' : MyColumn(column=model.System.model,col_type='string'),
  942. 'SerialNumber': MyColumn(column=model.System.serial, col_type='string'),
  943. 'Memory' : MyColumn(column=model.System.memory,col_type='numeric'),
  944. 'Hypervisor': MyColumn(column=model.Hypervisor.hypervisor, col_type='string', relations='hypervisor'),
  945. 'NumaNodes' : MyColumn(column=model.Numa.nodes, col_type='numeric', relations='numa'),
  946. 'Notes' : AliasedColumn(column_name='text',
  947. col_type='string', relations=[model.Note],
  948. onclause=model.System.notes),
  949. 'User' : AliasedColumn(column_name='user_name',
  950. relations=[model.User],
  951. col_type='string',
  952. onclause=model.System.user),
  953. 'Owner' : AliasedColumn(column_name='user_name',
  954. col_type='string', relations=[model.User],
  955. onclause=model.System.owner),
  956. 'Status' : MyColumn(column=model.System.status, col_type='string'),
  957. 'Arch' : MyColumn(column=model.Arch.arch,
  958. col_type='string',
  959. relations=[model.System.arch],
  960. eagerload=False,),
  961. 'Type' : MyColumn(column=model.System.type, col_type='string'),
  962. 'Reserved' : MyColumn(column=model.Reservation.start_time, col_type='date', relations='open_reservation'),
  963. 'PowerType' : MyColumn(column=model.PowerType.name, col_type='string',
  964. relations=[model.System.power, model.Power.power_type]),
  965. 'LoanedTo' : AliasedColumn(column_name='user_name',
  966. col_type='string',
  967. onclause=model.System.loaned,
  968. relations=[model.User]),
  969. 'LoanComment': MyColumn(
  970. column=model.System.loan_comment,
  971. col_type='string'),
  972. 'Group' : AliasedColumn(col_type='string',
  973. column_name='name',
  974. eagerload=False,
  975. onclause=model.System.pools,
  976. relations=[model.SystemPool],
  977. hidden=True),
  978. 'Pools' : AliasedColumn(col_type='string',
  979. column_name='name',
  980. eagerload=False,
  981. onclause=model.System.pools,
  982. relations=[model.SystemPool]),
  983. 'LabController' : AliasedColumn(column_name='fqdn',
  984. col_type='string', relations=[model.LabController],
  985. onclause=model.System.lab_controller),
  986. }
  987. search_values_dict = {'Status' : lambda: [status for status in
  988. model.SystemStatus.values() if status != 'Removed'],
  989. 'Type' : lambda: model.SystemType.values(),
  990. 'Hypervisor': lambda: [''] + model.Hypervisor.get_all_names(),
  991. }
  992. @classmethod
  993. def filter_by_exact_date_for_datetime(cls, col, date_string):
  994. """Filters using a given date on datetime columns"""
  995. if not date_string:
  996. return col == None
  997. else:
  998. date = datetime.datetime.strptime(date_string, '%Y-%m-%d')
  999. return and_(col >= date, col < date + datetime.timedelta(days=1))
  1000. added_is_filter = filter_by_exact_date_for_datetime
  1001. reserved_is_filter = filter_by_exact_date_for_datetime
  1002. lastinventoried_is_filter = filter_by_exact_date_for_datetime
  1003. @classmethod
  1004. def filter_by_future_date_for_datetime(cls, col, date_string):
  1005. if not date_string:
  1006. return col == None
  1007. else:
  1008. date = datetime.datetime.strptime(date_string, '%Y-%m-%d')
  1009. return col >= date + datetime.timedelta(days=1)
  1010. added_after_filter = filter_by_future_date_for_datetime
  1011. reserved_after_filter = filter_by_future_date_for_datetime
  1012. lastinventoried_after_filter = filter_by_future_date_for_datetime
  1013. @classmethod
  1014. def filter_by_past_date_for_datetime(cls, col, date_string):
  1015. if not date_string:
  1016. return col == None
  1017. else:
  1018. date = datetime.datetime.strptime(date_string, '%Y-%m-%d')
  1019. return col < date
  1020. added_before_filter = filter_by_past_date_for_datetime
  1021. reserved_before_filter = filter_by_past_date_for_datetime
  1022. lastinventoried_before_filter = filter_by_past_date_for_datetime
  1023. @classmethod
  1024. def arch_is_not_filter(cls,col,val):
  1025. """
  1026. arch_is_not_filter is a function dynamically called from append_results.
  1027. It serves to provide a table column operation specific method of filtering results of System/Arch
  1028. """
  1029. if not val:
  1030. return or_(col != None, col != val)
  1031. else:
  1032. #If anyone knows of a better way to do this, by all means...
  1033. query = model.System.query.filter(model.System.arch.any(model.Arch.arch == val))
  1034. ids = [r.id for r in query]
  1035. return not_(model.System.id.in_(ids))
  1036. @classmethod
  1037. def notes_contains_filter(cls, col, val, **kw):
  1038. """
  1039. notes_contains_filter is a function dynamically called from append_results.
  1040. It serves to provide a table column operation specific method of filtering results of System/Notes
  1041. In this case it specifically searches case-insensitively through System/Notes
  1042. """
  1043. query = model.System.query.filter(model.System.notes.any(model.Note.text.ilike(val)))
  1044. return model.System.id.in_([x.id for x in query])
  1045. class Recipe(SystemObject):
  1046. search = RecipeSearch
  1047. searchable_columns = {
  1048. 'Id' : MyColumn(col_type='numeric', column=model.MachineRecipe.id),
  1049. 'Whiteboard' : MyColumn(col_type='string', column=model.Recipe.whiteboard),
  1050. 'System' : MyColumn(col_type='string', column=model.RecipeResource.fqdn,
  1051. relations=[model.Recipe.resource]),
  1052. 'Arch' : MyColumn(col_type='string', column=model.Arch.arch,
  1053. relations=[model.Recipe.distro_tree, model.Di

Large files files are truncated, but you can click here to view the full file