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