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