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