Package Gnumed :: Package wxpython :: Module gmDataMiningWidgets
[frames] | no frames]

Source Code for Module Gnumed.wxpython.gmDataMiningWidgets

  1  """GNUmed data mining related widgets.""" 
  2   
  3  #================================================================ 
  4  __author__ = 'karsten.hilbert@gmx.net' 
  5  __license__ = 'GPL v2 or later (details at http://www.gnu.org)' 
  6   
  7   
  8  # stdlib 
  9  import sys 
 10  import os 
 11  import fileinput 
 12  import logging 
 13   
 14   
 15  # 3rd party 
 16  import wx 
 17   
 18   
 19  # GNUmed 
 20  if __name__ == '__main__': 
 21          sys.path.insert(0, '../../') 
 22  from Gnumed.pycommon import gmDispatcher 
 23  from Gnumed.pycommon import gmMimeLib 
 24  from Gnumed.pycommon import gmTools 
 25  from Gnumed.pycommon import gmPG2 
 26  from Gnumed.pycommon import gmMatchProvider 
 27  from Gnumed.pycommon import gmI18N 
 28  from Gnumed.pycommon import gmNetworkTools 
 29   
 30  from Gnumed.business import gmPerson 
 31  from Gnumed.business import gmDataMining 
 32  from Gnumed.business import gmPersonSearch 
 33   
 34  from Gnumed.wxpython import gmGuiHelpers 
 35  from Gnumed.wxpython import gmListWidgets 
 36   
 37   
 38  _log = logging.getLogger('gm.ui') 
 39  #================================================================ 
40 -class cPatientListingCtrl(gmListWidgets.cReportListCtrl):
41
42 - def __init__(self, *args, **kwargs):
43 """<patient_key> must index or name a column in self.__data""" 44 try: 45 self.patient_key = kwargs['patient_key'] 46 del kwargs['patient_key'] 47 except KeyError: 48 self.patient_key = None 49 50 gmListWidgets.cReportListCtrl.__init__(self, *args, **kwargs) 51 52 self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._on_list_item_activated, self)
53 #------------------------------------------------------------
54 - def __get_patient_pk_column(self, data=None):
55 if self.patient_key is not None: 56 try: 57 data[self.patient_key] 58 return self.patient_key 59 except (KeyError, IndexError, TypeError): 60 _log.error('misconfigured identifier column <%s>', self.patient_key) 61 62 _log.debug('identifier column not configured, trying to detect') 63 64 if data.has_key('pk_patient'): 65 return u'pk_patient' 66 67 if data.has_key('fk_patient'): 68 return u'fk_patient' 69 70 if data.has_key('pk_identity'): 71 return u'pk_identity' 72 73 if data.has_key('fk_identity'): 74 return u'fk_identity' 75 76 if data.has_key('id_identity'): 77 return u'id_identity' 78 79 return gmListWidgets.get_choices_from_list ( 80 parent = self, 81 msg = _( 82 'The report result list does not contain any columns\n' 83 'named "%s", "pk_patient", or "pk_identity".\n' 84 '\n' 85 'Select the column which contains patient IDs:\n' 86 ) % self.patient_key, 87 caption = _('Choose column from query results ...'), 88 choices = data.keys(), 89 columns = [_('Column name')], 90 single_selection = True 91 )
92 #------------------------------------------------------------ 93 # event handling 94 #------------------------------------------------------------
95 - def _on_list_item_activated(self, evt):
96 data = self.get_selected_item_data(only_one = True) 97 pk_pat_col = self.__get_patient_pk_column(data = data) 98 99 if pk_pat_col is None: 100 gmDispatcher.send(signal = 'statustext', msg = _('List not known to be patient-related.')) 101 return 102 103 pat_data = data[pk_pat_col] 104 try: 105 pat_pk = int(pat_data) 106 pat = gmPerson.cIdentity(aPK_obj = pat_pk) 107 except (ValueError, TypeError): 108 searcher = gmPersonSearch.cPatientSearcher_SQL() 109 idents = searcher.get_identities(pat_data) 110 if len(idents) == 0: 111 gmDispatcher.send(signal = 'statustext', msg = _('No matching patient found.')) 112 return 113 if len(idents) == 1: 114 pat = idents[0] 115 else: 116 from Gnumed.wxpython import gmPatSearchWidgets 117 dlg = gmPatSearchWidgets.cSelectPersonFromListDlg(parent=wx.GetTopLevelParent(self), id=-1) 118 dlg.set_persons(persons=idents) 119 result = dlg.ShowModal() 120 if result == wx.ID_CANCEL: 121 dlg.Destroy() 122 return 123 pat = dlg.get_selected_person() 124 dlg.Destroy() 125 126 from Gnumed.wxpython import gmPatSearchWidgets 127 gmPatSearchWidgets.set_active_patient(patient = pat)
128 129 #================================================================ 130 from Gnumed.wxGladeWidgets import wxgPatientListingPnl 131
132 -class cPatientListingPnl(wxgPatientListingPnl.wxgPatientListingPnl):
133
134 - def __init__(self, *args, **kwargs):
135 136 try: 137 button_defs = kwargs['button_defs'][:5] 138 del kwargs['button_defs'] 139 except KeyError: 140 button_defs = [] 141 142 try: 143 msg = kwargs['message'] 144 del kwargs['message'] 145 except KeyError: 146 msg = None 147 148 wxgPatientListingPnl.wxgPatientListingPnl.__init__(self, *args, **kwargs) 149 150 if msg is not None: 151 self._lbl_msg.SetLabel(msg) 152 153 buttons = [self._BTN_1, self._BTN_2, self._BTN_3, self._BTN_4, self._BTN_5] 154 for idx in range(len(button_defs)): 155 button_def = button_defs[idx] 156 if button_def['label'].strip() == u'': 157 continue 158 buttons[idx].SetLabel(button_def['label']) 159 buttons[idx].SetToolTipString(button_def['tooltip']) 160 buttons[idx].Enable(True) 161 162 self.Fit()
163 #------------------------------------------------------------ 164 # event handling 165 #------------------------------------------------------------
166 - def _on_BTN_1_pressed(self, event):
167 event.Skip()
168 #------------------------------------------------------------
169 - def _on_BTN_2_pressed(self, event):
170 event.Skip()
171 #------------------------------------------------------------
172 - def _on_BTN_3_pressed(self, event):
173 event.Skip()
174 #------------------------------------------------------------
175 - def _on_BTN_4_pressed(self, event):
176 event.Skip()
177 #------------------------------------------------------------
178 - def _on_BTN_5_pressed(self, event):
179 event.Skip()
180 181 #================================================================ 182 from Gnumed.wxGladeWidgets import wxgDataMiningPnl 183
184 -class cDataMiningPnl(wxgDataMiningPnl.wxgDataMiningPnl):
185
186 - def __init__(self, *args, **kwargs):
187 wxgDataMiningPnl.wxgDataMiningPnl.__init__(self, *args, **kwargs) 188 189 self.__init_ui() 190 191 # make me a file drop target 192 dt = gmGuiHelpers.cFileDropTarget(self) 193 self.SetDropTarget(dt)
194 #--------------------------------------------------------
195 - def __init_ui(self):
196 mp = gmMatchProvider.cMatchProvider_SQL2 ( 197 queries = [u""" 198 SELECT DISTINCT ON (label) 199 cmd, 200 label 201 FROM cfg.report_query 202 WHERE 203 label %(fragment_condition)s 204 OR 205 cmd %(fragment_condition)s 206 """] 207 ) 208 mp.setThresholds(2,3,5) 209 self._PRW_report_name.matcher = mp 210 self._PRW_report_name.add_callback_on_selection(callback = self._on_report_selected) 211 self._PRW_report_name.add_callback_on_lose_focus(callback = self._auto_load_report)
212 #--------------------------------------------------------
213 - def _auto_load_report(self, *args, **kwargs):
214 if self._TCTRL_query.GetValue() == u'': 215 if self._PRW_report_name.GetData() is not None: 216 self._TCTRL_query.SetValue(self._PRW_report_name.GetData()) 217 self._BTN_run.SetFocus()
218 #--------------------------------------------------------
219 - def _on_report_selected(self, *args, **kwargs):
220 self._TCTRL_query.SetValue(self._PRW_report_name.GetData()) 221 self._BTN_run.SetFocus()
222 #-------------------------------------------------------- 223 # file drop target API 224 #--------------------------------------------------------
225 - def add_filenames(self, filenames):
226 # act on first file only 227 fname = filenames[0] 228 _log.debug('importing SQL from <%s>', fname) 229 # act on text files only 230 mime_type = gmMimeLib.guess_mimetype(fname) 231 _log.debug('mime type: %s', mime_type) 232 if not mime_type.startswith('text/'): 233 _log.debug('not a text file') 234 gmDispatcher.send(signal='statustext', msg = _('Cannot read SQL from [%s]. Not a text file.') % fname, beep = True) 235 return False 236 # # act on "small" files only 237 # stat_val = os.stat(fname) 238 # if stat_val.st_size > 5000: 239 # gmDispatcher.send(signal='statustext', msg = _('Cannot read SQL from [%s]. File too big (> 2000 bytes).') % fname, beep = True) 240 # return False 241 # all checks passed 242 for line in fileinput.input(fname): 243 self._TCTRL_query.AppendText(line)
244 #-------------------------------------------------------- 245 # notebook plugin API 246 #--------------------------------------------------------
247 - def repopulate_ui(self):
248 pass
249 #-------------------------------------------------------- 250 # event handlers 251 #--------------------------------------------------------
252 - def _on_contribute_button_pressed(self, evt):
253 report = self._PRW_report_name.GetValue().strip() 254 if report == u'': 255 gmDispatcher.send(signal = 'statustext', msg = _('Report must have a name for contribution.'), beep = False) 256 return 257 258 query = self._TCTRL_query.GetValue().strip() 259 if query == u'': 260 gmDispatcher.send(signal = 'statustext', msg = _('Report must have a query for contribution.'), beep = False) 261 return 262 263 do_it = gmGuiHelpers.gm_show_question ( 264 _( 'Be careful that your contribution (the query itself) does\n' 265 'not contain any person-identifiable search parameters.\n' 266 '\n' 267 'Note, however, that no query result data whatsoever\n' 268 'is included in the contribution that will be sent.\n' 269 '\n' 270 'Are you sure you wish to send this query to\n' 271 'the gnumed community mailing list?\n' 272 ), 273 _('Contributing custom report') 274 ) 275 if not do_it: 276 return 277 278 auth = {'user': gmNetworkTools.default_mail_sender, 'password': u'gnumed-at-gmx-net'} 279 msg = u"""--- This is a report definition contributed by a GNUmed user. 280 281 --- Save it as a text file and drop it onto the Report Generator 282 --- inside GNUmed in order to take advantage of the contribution. 283 284 ---------------------------------------- 285 286 --- %s 287 288 %s 289 290 ---------------------------------------- 291 292 --- The GNUmed client. 293 """ % (report, query) 294 295 if not gmNetworkTools.send_mail ( 296 sender = u'GNUmed Report Generator <gnumed@gmx.net>', 297 receiver = [u'gnumed-devel@gnu.org'], 298 subject = u'user contributed report', 299 message = msg, 300 encoding = gmI18N.get_encoding(), 301 server = gmNetworkTools.default_mail_server, 302 auth = auth 303 ): 304 gmDispatcher.send(signal = 'statustext', msg = _('Unable to send mail. Cannot contribute report [%s] to GNUmed community.') % report, beep = True) 305 return False 306 307 gmDispatcher.send(signal = 'statustext', msg = _('Thank you for your contribution to the GNUmed community!'), beep = False) 308 return True
309 #--------------------------------------------------------
310 - def _on_schema_button_pressed(self, evt):
311 # will block when called in text mode (that is, from a terminal, too !) 312 gmNetworkTools.open_url_in_browser(url = u'http://wiki.gnumed.de/bin/view/Gnumed/DatabaseSchema')
313 #--------------------------------------------------------
314 - def _on_delete_button_pressed(self, evt):
315 report = self._PRW_report_name.GetValue().strip() 316 if report == u'': 317 return True 318 if gmDataMining.delete_report_definition(name=report): 319 self._PRW_report_name.SetText() 320 self._TCTRL_query.SetValue(u'') 321 gmDispatcher.send(signal='statustext', msg = _('Deleted report definition [%s].') % report, beep=False) 322 return True 323 gmDispatcher.send(signal='statustext', msg = _('Error deleting report definition [%s].') % report, beep=True) 324 return False
325 #--------------------------------------------------------
326 - def _on_clear_button_pressed(self, evt):
327 self._PRW_report_name.SetText() 328 self._TCTRL_query.SetValue(u'') 329 self._LCTRL_result.set_columns()
330 #--------------------------------------------------------
331 - def _on_save_button_pressed(self, evt):
332 report = self._PRW_report_name.GetValue().strip() 333 if report == u'': 334 gmDispatcher.send(signal='statustext', msg = _('Cannot save report definition without name.'), beep=True) 335 return False 336 query = self._TCTRL_query.GetValue().strip() 337 if query == u'': 338 gmDispatcher.send(signal='statustext', msg = _('Cannot save report definition without query.'), beep=True) 339 return False 340 # FIXME: check for exists and ask for permission 341 if gmDataMining.save_report_definition(name=report, query=query, overwrite=True): 342 gmDispatcher.send(signal='statustext', msg = _('Saved report definition [%s].') % report, beep=False) 343 return True 344 gmDispatcher.send(signal='statustext', msg = _('Error saving report definition [%s].') % report, beep=True) 345 return False
346 #--------------------------------------------------------
347 - def _on_visualize_button_pressed(self, evt):
348 349 try: 350 # better fail early 351 import Gnuplot 352 except ImportError: 353 gmGuiHelpers.gm_show_info ( 354 aMessage = _('Cannot import "Gnuplot" python module.'), 355 aTitle = _('Query result visualizer') 356 ) 357 return 358 359 x_col = gmListWidgets.get_choices_from_list ( 360 parent = self, 361 msg = _('Choose a column to be used as the X-Axis:'), 362 caption = _('Choose column from query results ...'), 363 choices = self.query_results[0].keys(), 364 columns = [_('Column name')], 365 single_selection = True 366 ) 367 if x_col is None: 368 return 369 370 y_col = gmListWidgets.get_choices_from_list ( 371 parent = self, 372 msg = _('Choose a column to be used as the Y-Axis:'), 373 caption = _('Choose column from query results ...'), 374 choices = self.query_results[0].keys(), 375 columns = [_('Column name')], 376 single_selection = True 377 ) 378 if y_col is None: 379 return 380 381 # FIXME: support debugging (debug=1) depending on --debug 382 gp = Gnuplot.Gnuplot(persist=1) 383 if self._PRW_report_name.GetValue().strip() != u'': 384 gp.title(_('GNUmed report: %s') % self._PRW_report_name.GetValue().strip()[:40]) 385 else: 386 gp.title(_('GNUmed report results')) 387 gp.xlabel(x_col) 388 gp.ylabel(y_col) 389 try: 390 gp.plot([ [r[x_col], r[y_col]] for r in self.query_results ]) 391 except StandardError: 392 _log.exception('unable to plot results from [%s:%s]' % (x_col, y_col)) 393 gmDispatcher.send(signal = 'statustext', msg = _('Error plotting data.'), beep = True) 394 395 return
396 #--------------------------------------------------------
397 - def _on_run_button_pressed(self, evt):
398 399 self._BTN_visualize.Enable(False) 400 401 user_query = self._TCTRL_query.GetValue().strip().strip(';') 402 if user_query == u'': 403 return True 404 405 # FIXME: make LIMIT configurable 406 limit = u'1001' 407 408 wrapper_query = u""" 409 SELECT * 410 FROM ( 411 %%s 412 ) AS user_query 413 LIMIT %s 414 """ % limit 415 416 # does user want to insert current patient ID ? 417 patient_id_token = u'$<ID_active_patient>$' 418 if user_query.find(patient_id_token) != -1: 419 # she does, but is it possible ? 420 curr_pat = gmPerson.gmCurrentPatient() 421 if not curr_pat.connected: 422 gmGuiHelpers.gm_show_error ( 423 aMessage = _( 424 'This query requires a patient to be\n' 425 'active in the client.\n' 426 '\n' 427 'Please activate the patient you are interested\n' 428 'in and re-run the query.\n' 429 ), 430 aTitle = _('Active patient query') 431 ) 432 return False 433 wrapper_query = u""" 434 SELECT 435 %s AS pk_patient, 436 * 437 FROM ( 438 %%s 439 ) AS user_query 440 LIMIT %s 441 """ % (str(curr_pat.ID), limit) 442 user_query = user_query.replace(patient_id_token, str(curr_pat.ID)) 443 444 self._LCTRL_result.set_columns() 445 446 query = wrapper_query % user_query 447 try: 448 # read-only for safety reasons 449 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': query}], get_col_idx = True) 450 except StandardError: 451 _log.exception('report query failed') 452 self._LCTRL_result.set_columns([_('Error')]) 453 t, v = sys.exc_info()[:2] 454 rows = [ 455 [_('The query failed.')], 456 [u''], 457 [unicode(t)] 458 ] 459 for line in str(v).decode(gmI18N.get_encoding()).split('\n'): 460 rows.append([line]) 461 rows.append([u'']) 462 for line in user_query.split('\n'): 463 rows.append([line]) 464 self._LCTRL_result.set_string_items(rows) 465 self._LCTRL_result.set_column_widths() 466 gmDispatcher.send('statustext', msg = _('The query failed.'), beep = True) 467 return False 468 469 if len(rows) == 0: 470 self._LCTRL_result.set_columns([_('Results')]) 471 self._LCTRL_result.set_string_items([[_('Report returned no data.')]]) 472 self._LCTRL_result.set_column_widths() 473 gmDispatcher.send('statustext', msg = _('No data returned for this report.'), beep = True) 474 return True 475 476 gmDispatcher.send(signal = 'statustext', msg = _('Found %s results.') % len(rows)) 477 478 if len(rows) == 1001: 479 gmGuiHelpers.gm_show_info ( 480 aMessage = _( 481 'This query returned at least 1001 results.\n' 482 '\n' 483 'GNUmed will only show the first 1000 rows.\n' 484 '\n' 485 'You may want to narrow down the WHERE conditions\n' 486 'or use LIMIT and OFFSET to batchwise go through\n' 487 'all the matching rows.' 488 ), 489 aTitle = _('Report Generator') 490 ) 491 rows = rows[:-1] # make it true :-) 492 493 # swap (col_name, col_idx) to (col_idx, col_name) as needed by 494 # set_columns() and sort them according to position-in-query 495 cols = [ (value, key) for key, value in idx.items() ] 496 cols.sort() 497 cols = [ pair[1] for pair in cols ] 498 self._LCTRL_result.set_columns(cols) 499 for row in rows: 500 try: 501 label = unicode(gmTools.coalesce(row[0], u'')).replace('\n', '<LF>').replace('\r', '<CR>') 502 except UnicodeDecodeError: 503 label = _('not unicode()able') 504 if len(label) > 150: 505 label = label[:150] + gmTools.u_ellipsis 506 row_num = self._LCTRL_result.InsertStringItem(sys.maxint, label = label) 507 for col_idx in range(1, len(row)): 508 try: 509 label = unicode(gmTools.coalesce(row[col_idx], u'')).replace('\n', '<LF>').replace('\r', '<CR>')[:250] 510 except UnicodeDecodeError: 511 label = _('not unicode()able') 512 if len(label) > 150: 513 label = label[:150] + gmTools.u_ellipsis 514 self._LCTRL_result.SetStringItem ( 515 index = row_num, 516 col = col_idx, 517 label = label 518 ) 519 # must be called explicitely, because string items are set above without calling set_string_items 520 self._LCTRL_result._invalidate_sorting_metadata() 521 self._LCTRL_result.set_column_widths() 522 self._LCTRL_result.set_data(data = rows) 523 524 self.query_results = rows 525 self._BTN_visualize.Enable(True) 526 527 return True
528 #================================================================ 529 # main 530 #---------------------------------------------------------------- 531 if __name__ == '__main__': 532 from Gnumed.pycommon import gmI18N, gmDateTime 533 534 gmI18N.activate_locale() 535 gmI18N.install_domain() 536 gmDateTime.init() 537 538 #------------------------------------------------------------
539 - def test_pat_list_ctrl():
540 app = wx.PyWidgetTester(size = (400, 500)) 541 lst = cPatientListingCtrl(app.frame, patient_key = 0) 542 lst.set_columns(['name', 'comment']) 543 lst.set_string_items([ 544 ['Kirk', 'Kirk by name'], 545 ['#12', 'Kirk by ID'], 546 ['unknown', 'unknown patient'] 547 ]) 548 # app.SetWidget(cPatientListingCtrl, patient_key = 0) 549 app.frame.Show() 550 app.MainLoop()
551 #------------------------------------------------------------ 552 553 test_pat_list_ctrl() 554