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