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