Package Gnumed :: Package wxpython :: Module gmPatSearchWidgets |
[frames] | no frames]

# Source Code for Module Gnumed.wxpython.gmPatSearchWidgets

       1  #  coding: latin-1
       2  """GNUmed quick person search widgets.
       3
       4  This widget allows to search for persons based on the
       5  critera name, date of birth and person ID. It goes to
       6  considerable lengths to understand the user's intent from
       7  her input. For that to work well we need per-culture
       8  query generators. However, there's always the fallback
       9  generator.
      10  """
      11  #============================================================
      12  __author__ = "K.Hilbert <Karsten.Hilbert@gmx.net>"
      13  __license__ = 'GPL v2 or later (for details see http://www.gnu.org/)'
      14
      15  import sys, os.path, glob, re as regex, logging
      16
      17
      18  import wx
      19
      20
      21  if __name__ == '__main__':
      22          sys.path.insert(0, '../../')
      23          from Gnumed.pycommon import gmLog2
      24  from Gnumed.pycommon import gmDispatcher
      25  from Gnumed.pycommon import gmDateTime
      26  from Gnumed.pycommon import gmTools
      27  from Gnumed.pycommon import gmPG2
      28  from Gnumed.pycommon import gmI18N
      29  from Gnumed.pycommon import gmCfg
      30  from Gnumed.pycommon import gmMatchProvider
      31  from Gnumed.pycommon import gmCfg2
      32  from Gnumed.pycommon import gmNetworkTools
      33
      34  from Gnumed.business import gmPerson
      35  from Gnumed.business import gmStaff
      36  from Gnumed.business import gmKVK
      37  from Gnumed.business import gmPraxis
      38  from Gnumed.business import gmCA_MSVA
      39  from Gnumed.business import gmPersonSearch
      40  from Gnumed.business import gmProviderInbox
      41
      42  from Gnumed.wxpython import gmGuiHelpers
      43  from Gnumed.wxpython import gmAuthWidgets
      44  from Gnumed.wxpython import gmRegetMixin
      45  from Gnumed.wxpython import gmEditArea
      46  from Gnumed.wxpython import gmPhraseWheel
      47  from Gnumed.wxpython.gmPersonCreationWidgets import create_new_person
      48
      49
      50  _log = logging.getLogger('gm.person')
      51
      52  _cfg = gmCfg2.gmCfgData()
      53
      54  ID_PatPickList = wx.NewId()
      55  ID_BTN_AddNew = wx.NewId()
      56
      57  #============================================================
      58 -def merge_patients(parent=None):
      59          dlg = cMergePatientsDlg(parent, -1)
      60          result = dlg.ShowModal()
      61  #============================================================
      62  from Gnumed.wxGladeWidgets import wxgMergePatientsDlg
      63
      64 -class cMergePatientsDlg(wxgMergePatientsDlg.wxgMergePatientsDlg):
      65
      66 -        def __init__(self, *args, **kwargs):
      67                  wxgMergePatientsDlg.wxgMergePatientsDlg.__init__(self, *args, **kwargs)
      68
      69                  curr_pat = gmPerson.gmCurrentPatient()
      70                  if curr_pat.connected:
      71                          self._TCTRL_patient1.person = curr_pat
      72                          self._TCTRL_patient1._display_name()
      73                          self._RBTN_patient1.SetValue(True)
      74          #--------------------------------------------------------
      75 -        def _on_merge_button_pressed(self, event):
      76
      77                  if self._TCTRL_patient1.person is None:
      78                          return
      79
      80                  if self._TCTRL_patient2.person is None:
      81                          return
      82
      83                  if self._RBTN_patient1.GetValue():
      84                          patient2keep = self._TCTRL_patient1.person
      85                          patient2merge = self._TCTRL_patient2.person
      86                  else:
      87                          patient2keep = self._TCTRL_patient2.person
      88                          patient2merge = self._TCTRL_patient1.person
      89
      90                  if patient2merge['lastnames'] == u'Kirk':
      91                          if _cfg.get(option = 'debug'):
      92                                  gmNetworkTools.open_url_in_browser(url = 'http://en.wikipedia.org/wiki/File:Picard_as_Locutus.jpg')
      93                                  gmGuiHelpers.gm_show_info(_('\n\nYou will be assimilated.\n\n'), _('The Borg'))
      94                                  return
      95                          else:
      96                                  gmDispatcher.send(signal = 'statustext', msg = _('Cannot merge Kirk into another patient.'), beep = True)
      97                                  return
      98
      99                  doit = gmGuiHelpers.gm_show_question (
     100                          aMessage = _(
     101                                  'Are you positively sure you want to merge patient\n\n'
     102                                  ' #%s: %s (%s, %s)\n\n'
     103                                  'into patient\n\n'
     104                                  ' #%s: %s (%s, %s) ?\n\n'
     105                                  'Note that this action can ONLY be reversed by a laborious\n'
     106                                  'manual process requiring in-depth knowledge about databases\n'
     107                                  'and the patients in question !\n'
     108                          ) % (
     109                                  patient2merge.ID,
     110                                  patient2merge['description_gender'],
     111                                  patient2merge['gender'],
     112                                  patient2merge.get_formatted_dob(format = '%Y %b %d', encoding = gmI18N.get_encoding()),
     113                                  patient2keep.ID,
     114                                  patient2keep['description_gender'],
     115                                  patient2keep['gender'],
     116                                  patient2keep.get_formatted_dob(format = '%Y %b %d', encoding = gmI18N.get_encoding())
     117                          ),
     118                          aTitle = _('Merging patients: confirmation'),
     119                          cancel_button = False
     120                  )
     121                  if not doit:
     122                          return
     123
     124                  conn = gmAuthWidgets.get_dbowner_connection(procedure = _('Merging patients'))
     125                  if conn is None:
     126                          return
     127
     128                  success, msg = patient2keep.assimilate_identity(other_identity = patient2merge, link_obj = conn)
     129                  conn.close()
     130                  if not success:
     131                          gmDispatcher.send(signal = 'statustext', msg = msg, beep = True)
     132                          return
     133
     134                  # announce success, offer to activate kept patient if not active
     135                  doit = gmGuiHelpers.gm_show_question (
     136                          aMessage = _(
     137                                  'The patient\n'
     138                                  '\n'
     139                                  ' #%s: %s (%s, %s)\n'
     140                                  '\n'
     141                                  'has successfully been merged into\n'
     142                                  '\n'
     143                                  ' #%s: %s (%s, %s)\n'
     144                                  '\n'
     145                                  '\n'
     146                                  'Do you want to activate that patient\n'
     147                                  'now for further modifications ?\n'
     148                          ) % (
     149                                  patient2merge.ID,
     150                                  patient2merge['description_gender'],
     151                                  patient2merge['gender'],
     152                                  patient2merge.get_formatted_dob(format = '%Y %b %d', encoding = gmI18N.get_encoding()),
     153                                  patient2keep.ID,
     154                                  patient2keep['description_gender'],
     155                                  patient2keep['gender'],
     156                                  patient2keep.get_formatted_dob(format = '%Y %b %d', encoding = gmI18N.get_encoding())
     157                          ),
     158                          aTitle = _('Merging patients: success'),
     159                          cancel_button = False
     160                  )
     161                  if doit:
     162                          if not isinstance(patient2keep, gmPerson.gmCurrentPatient):
     163                                  wx.CallAfter(set_active_patient, patient = patient2keep)
     164
     165                  if self.IsModal():
     166                          self.EndModal(wx.ID_OK)
     167                  else:
     168                          self.Close()
     169  #============================================================
     170  from Gnumed.wxGladeWidgets import wxgSelectPersonFromListDlg
     171
     172 -class cSelectPersonFromListDlg(wxgSelectPersonFromListDlg.wxgSelectPersonFromListDlg):
     173
     174 -        def __init__(self, *args, **kwargs):
     175                  wxgSelectPersonFromListDlg.wxgSelectPersonFromListDlg.__init__(self, *args, **kwargs)
     176
     177                  self.__cols = [
     178                          _('Title'),
     179                          _('Lastname'),
     180                          _('Firstname'),
     181                          _('Nickname'),
     182                          _('DOB'),
     183                          _('Gender'),
     184                          _('last visit'),
     185                          _('found via')
     186                  ]
     187                  self.__init_ui()
     188          #--------------------------------------------------------
     189 -        def __init_ui(self):
     190                  for col in range(len(self.__cols)):
     191                          self._LCTRL_persons.InsertColumn(col, self.__cols[col])
     192          #--------------------------------------------------------
     193 -        def set_persons(self, persons=None):
     194                  self._LCTRL_persons.DeleteAllItems()
     195
     196                  pos = len(persons) + 1
     197                  if pos == 1:
     198                          return False
     199
     200                  for person in persons:
     201                          row_num = self._LCTRL_persons.InsertStringItem(pos, label = gmTools.coalesce(person['title'], ''))
     202                          self._LCTRL_persons.SetStringItem(index = row_num, col = 1, label = person['lastnames'])
     203                          self._LCTRL_persons.SetStringItem(index = row_num, col = 2, label = person['firstnames'])
     204                          self._LCTRL_persons.SetStringItem(index = row_num, col = 3, label = gmTools.coalesce(person['preferred'], ''))
     205                          self._LCTRL_persons.SetStringItem(index = row_num, col = 4, label = person.get_formatted_dob(format = '%Y %b %d', encoding = gmI18N.get_encoding()))
     206                          self._LCTRL_persons.SetStringItem(index = row_num, col = 5, label = gmTools.coalesce(person['l10n_gender'], '?'))
     207                          label = u''
     208                          if person.is_patient:
     209                                  enc = person.get_last_encounter()
     210                                  if enc is not None:
     211                                          label = u'%s (%s)' % (gmDateTime.pydt_strftime(enc['started'], '%Y %b %d'), enc['l10n_type'])
     212                          self._LCTRL_persons.SetStringItem(index = row_num, col = 6, label = label)
     213                          try:
     214                                  self._LCTRL_persons.SetStringItem(index = row_num, col = 7, label = person['match_type'])
     215                          except KeyError:
     216                                  _log.warning('cannot set match_type field')
     217                                  self._LCTRL_persons.SetStringItem(index = row_num, col = 7, label = u'??')
     218
     219                  for col in range(len(self.__cols)):
     220                          self._LCTRL_persons.SetColumnWidth(col=col, width=wx.LIST_AUTOSIZE)
     221
     222                  self._BTN_select.Enable(False)
     223                  self._LCTRL_persons.SetFocus()
     224                  self._LCTRL_persons.Select(0)
     225
     226                  self._LCTRL_persons.set_data(data=persons)
     227          #--------------------------------------------------------
     228 -        def get_selected_person(self):
     229                  return self._LCTRL_persons.get_item_data(self._LCTRL_persons.GetFirstSelected())
     230          #--------------------------------------------------------
     231          # event handlers
     232          #--------------------------------------------------------
     233 -        def _on_list_item_selected(self, evt):
     234                  self._BTN_select.Enable(True)
     235                  return
     236          #--------------------------------------------------------
     237 -        def _on_list_item_activated(self, evt):
     238                  self._BTN_select.Enable(True)
     239                  if self.IsModal():
     240                          self.EndModal(wx.ID_OK)
     241                  else:
     242                          self.Close()
     243  #============================================================
     244  from Gnumed.wxGladeWidgets import wxgSelectPersonDTOFromListDlg
     245
     246 -class cSelectPersonDTOFromListDlg(wxgSelectPersonDTOFromListDlg.wxgSelectPersonDTOFromListDlg):
     247
     248 -        def __init__(self, *args, **kwargs):
     249                  wxgSelectPersonDTOFromListDlg.wxgSelectPersonDTOFromListDlg.__init__(self, *args, **kwargs)
     250
     251                  self.__cols = [
     252                          _('Source'),
     253                          _('Lastname'),
     254                          _('Firstname'),
     255                          _('DOB'),
     256                          _('Gender')
     257                  ]
     258                  self.__init_ui()
     259          #--------------------------------------------------------
     260 -        def __init_ui(self):
     261                  for col in range(len(self.__cols)):
     262                          self._LCTRL_persons.InsertColumn(col, self.__cols[col])
     263          #--------------------------------------------------------
     264 -        def set_dtos(self, dtos=None):
     265                  self._LCTRL_persons.DeleteAllItems()
     266
     267                  pos = len(dtos) + 1
     268                  if pos == 1:
     269                          return False
     270
     271                  for rec in dtos:
     272                          row_num = self._LCTRL_persons.InsertStringItem(pos, label = rec['source'])
     273                          dto = rec['dto']
     274                          self._LCTRL_persons.SetStringItem(index = row_num, col = 1, label = dto.lastnames)
     275                          self._LCTRL_persons.SetStringItem(index = row_num, col = 2, label = dto.firstnames)
     276                          if dto.dob is None:
     277                                  self._LCTRL_persons.SetStringItem(index = row_num, col = 3, label = u'')
     278                          else:
     279                                  self._LCTRL_persons.SetStringItem(index = row_num, col = 3, label = gmDateTime.pydt_strftime(dto.dob, '%Y %b %d'))
     280                          self._LCTRL_persons.SetStringItem(index = row_num, col = 4, label = gmTools.coalesce(dto.gender, ''))
     281
     282                  for col in range(len(self.__cols)):
     283                          self._LCTRL_persons.SetColumnWidth(col=col, width=wx.LIST_AUTOSIZE)
     284
     285                  self._BTN_select.Enable(False)
     286                  self._LCTRL_persons.SetFocus()
     287                  self._LCTRL_persons.Select(0)
     288
     289                  self._LCTRL_persons.set_data(data=dtos)
     290          #--------------------------------------------------------
     291 -        def get_selected_dto(self):
     292                  return self._LCTRL_persons.get_item_data(self._LCTRL_persons.GetFirstSelected())
     293          #--------------------------------------------------------
     294          # event handlers
     295          #--------------------------------------------------------
     296 -        def _on_list_item_selected(self, evt):
     297                  self._BTN_select.Enable(True)
     298                  return
     299          #--------------------------------------------------------
     300 -        def _on_list_item_activated(self, evt):
     301                  self._BTN_select.Enable(True)
     302                  if self.IsModal():
     303                          self.EndModal(wx.ID_OK)
     304                  else:
     305                          self.Close()
     306
     307  #============================================================
     308 -def load_persons_from_ca_msva():
     309
     310          group = u'CA Medical Manager MSVA'
     311
     312          src_order = [
     313                  ('explicit', 'append'),
     314                  ('workbase', 'append'),
     315                  ('local', 'append'),
     316                  ('user', 'append'),
     317                  ('system', 'append')
     318          ]
     319          msva_files = _cfg.get (
     320                  group = group,
     321                  option = 'filename',
     322                  source_order = src_order
     323          )
     324          if msva_files is None:
     325                  return []
     326
     327          dtos = []
     328          for msva_file in msva_files:
     329                  try:
     330                          # FIXME: potentially return several persons per file
     331                          msva_dtos = gmCA_MSVA.read_persons_from_msva_file(filename = msva_file)
     332                  except StandardError:
     333                          gmGuiHelpers.gm_show_error (
     334                                  _(
     335                                  'Cannot load patient from Medical Manager MSVA file\n\n'
     336                                  ' [%s]'
     337                                  ) % msva_file,
     338                                  _('Activating MSVA patient')
     339                          )
     340                          _log.exception('cannot read patient from MSVA file [%s]' % msva_file)
     341                          continue
     342
     343                  dtos.extend([ {'dto': dto, 'source': dto.source} for dto in msva_dtos ])
     344                  #dtos.extend([ {'dto': dto} for dto in msva_dtos ])
     345
     346          return dtos
     347
     348  #============================================================
     349
     350 -def load_persons_from_xdt():
     351
     352          bdt_files = []
     353
     354          # some can be auto-detected
     355          # MCS/Isynet: $DRIVE:\Winacs\TEMP\BDTxx.tmp where xx is the workplace
     356          candidates = []
     357          drives = 'cdefghijklmnopqrstuvwxyz'
     358          for drive in drives:
     359                  candidate = drive + ':\Winacs\TEMP\BDT*.tmp'
     360                  candidates.extend(glob.glob(candidate))
     361          for candidate in candidates:
     362                  path, filename = os.path.split(candidate)
     363                  # FIXME: add encoding !
     364                  bdt_files.append({'file': candidate, 'source': 'MCS/Isynet %s' % filename[-6:-4]})
     365
     366          # some need to be configured
     367          # aggregate sources
     368          src_order = [
     369                  ('explicit', 'return'),
     370                  ('workbase', 'append'),
     371                  ('local', 'append'),
     372                  ('user', 'append'),
     373                  ('system', 'append')
     374          ]
     375          xdt_profiles = _cfg.get (
     376                  group = 'workplace',
     377                  option = 'XDT profiles',
     378                  source_order = src_order
     379          )
     380          if xdt_profiles is None:
     381                  return []
     382
     383          # first come first serve
     384          src_order = [
     385                  ('explicit', 'return'),
     386                  ('workbase', 'return'),
     387                  ('local', 'return'),
     388                  ('user', 'return'),
     389                  ('system', 'return')
     390          ]
     391          for profile in xdt_profiles:
     392                  name = _cfg.get (
     393                          group = 'XDT profile %s' % profile,
     394                          option = 'filename',
     395                          source_order = src_order
     396                  )
     397                  if name is None:
     398                          _log.error('XDT profile [%s] does not define a <filename>' % profile)
     399                          continue
     400                  encoding = _cfg.get (
     401                          group = 'XDT profile %s' % profile,
     402                          option = 'encoding',
     403                          source_order = src_order
     404                  )
     405                  if encoding is None:
     406                          _log.warning('xDT source profile [%s] does not specify an <encoding> for BDT file [%s]' % (profile, name))
     407                  source = _cfg.get (
     408                          group = 'XDT profile %s' % profile,
     409                          option = 'source',
     410                          source_order = src_order
     411                  )
     412                  dob_format = _cfg.get (
     413                          group = 'XDT profile %s' % profile,
     414                          option = 'DOB format',
     415                          source_order = src_order
     416                  )
     417                  if dob_format is None:
     418                          _log.warning('XDT profile [%s] does not define a date of birth format in <DOB format>' % profile)
     419                  bdt_files.append({'file': name, 'source': source, 'encoding': encoding, 'dob_format': dob_format})
     420
     421          dtos = []
     422          for bdt_file in bdt_files:
     423                  try:
     424                          # FIXME: potentially return several persons per file
     425                          dto = gmPerson.get_person_from_xdt (
     426                                  filename = bdt_file['file'],
     427                                  encoding = bdt_file['encoding'],
     428                                  dob_format = bdt_file['dob_format']
     429                          )
     430
     431                  except IOError:
     432                          gmGuiHelpers.gm_show_info (
     433                                  _(
     434                                  'Cannot access BDT file\n\n'
     435                                  ' [%s]\n\n'
     436                                  'to import patient.\n\n'
     437                                  'Please check your configuration.'
     438                                  ) % bdt_file,
     439                                  _('Activating xDT patient')
     440                          )
     441                          _log.exception('cannot access xDT file [%s]' % bdt_file['file'])
     442                          continue
     443                  except:
     444                          gmGuiHelpers.gm_show_error (
     445                                  _(
     446                                  'Cannot load patient from BDT file\n\n'
     447                                  ' [%s]'
     448                                  ) % bdt_file,
     449                                  _('Activating xDT patient')
     450                          )
     451                          _log.exception('cannot read patient from xDT file [%s]' % bdt_file['file'])
     452                          continue
     453
     454                  dtos.append({'dto': dto, 'source': gmTools.coalesce(bdt_file['source'], dto.source)})
     455
     456          return dtos
     457
     458  #============================================================
     459
     460 -def load_persons_from_pracsoft_au():
     461
     462          pracsoft_files = []
     463
     464          # try detecting PATIENTS.IN files
     465          candidates = []
     466          drives = 'cdefghijklmnopqrstuvwxyz'
     467          for drive in drives:
     468                  candidate = drive + ':\MDW2\PATIENTS.IN'
     469                  candidates.extend(glob.glob(candidate))
     470          for candidate in candidates:
     471                  drive, filename = os.path.splitdrive(candidate)
     472                  pracsoft_files.append({'file': candidate, 'source': 'PracSoft (AU): drive %s' % drive})
     473
     474          # add configured one(s)
     475          src_order = [
     476                  ('explicit', 'append'),
     477                  ('workbase', 'append'),
     478                  ('local', 'append'),
     479                  ('user', 'append'),
     480                  ('system', 'append')
     481          ]
     482          fnames = _cfg.get (
     483                  group = 'AU PracSoft PATIENTS.IN',
     484                  option = 'filename',
     485                  source_order = src_order
     486          )
     487
     488          src_order = [
     489                  ('explicit', 'return'),
     490                  ('user', 'return'),
     491                  ('system', 'return'),
     492                  ('local', 'return'),
     493                  ('workbase', 'return')
     494          ]
     495          source = _cfg.get (
     496                  group = 'AU PracSoft PATIENTS.IN',
     497                  option = 'source',
     498                  source_order = src_order
     499          )
     500
     501          if source is not None:
     502                  for fname in fnames:
     503                          fname = os.path.abspath(os.path.expanduser(fname))
     504                          if os.access(fname, os.R_OK):
     505                                  pracsoft_files.append({'file': os.path.expanduser(fname), 'source': source})
     506                          else:
     507                                  _log.error('cannot read [%s] in AU PracSoft profile' % fname)
     508
     509          # and parse them
     510          dtos = []
     511          for pracsoft_file in pracsoft_files:
     512                  try:
     513                          tmp = gmPerson.get_persons_from_pracsoft_file(filename = pracsoft_file['file'])
     514                  except:
     515                          _log.exception('cannot parse PracSoft file [%s]' % pracsoft_file['file'])
     516                          continue
     517                  for dto in tmp:
     518                          dtos.append({'dto': dto, 'source': pracsoft_file['source']})
     519
     520          return dtos
     521  #============================================================
     522 -def load_persons_from_kvks():
     523
     524          dbcfg = gmCfg.cCfgSQL()
     525          kvk_dir = os.path.abspath(os.path.expanduser(dbcfg.get2 (
     526                  option = 'DE.KVK.spool_dir',
     527                  workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
     528                  bias = 'workplace',
     529                  default = u'/var/spool/kvkd/'
     530          )))
     531          dtos = []
     532          for dto in gmKVK.get_available_kvks_as_dtos(spool_dir = kvk_dir):
     533                  dtos.append({'dto': dto, 'source': 'KVK'})
     534
     535          return dtos
     536  #============================================================
     537 -def get_person_from_external_sources(parent=None, search_immediately=False, activate_immediately=False):
     538          """Load patient from external source.
     539
     540          - scan external sources for candidates
     541          - let user select source
     542            - if > 1 available: always
     543            - if only 1 available: depending on search_immediately
     544          - search for patients matching info from external source
     545          - if more than one match:
     546            - let user select patient
     547          - if no match:
     548            - create patient
     549          - activate patient
     550          """
     551          # get DTOs from interfaces
     552          dtos = []
     553          dtos.extend(load_persons_from_xdt())
     554          dtos.extend(load_persons_from_pracsoft_au())
     555          dtos.extend(load_persons_from_kvks())
     556          dtos.extend(load_persons_from_ca_msva())
     557
     558          # no external persons
     559          if len(dtos) == 0:
     560                  gmDispatcher.send(signal='statustext', msg=_('No patients found in external sources.'))
     561                  return None
     562
     563          # one external patient with DOB - already active ?
     564          if (len(dtos) == 1) and (dtos[0]['dto'].dob is not None):
     565                  dto = dtos[0]['dto']
     566                  # is it already the current patient ?
     567                  curr_pat = gmPerson.gmCurrentPatient()
     568                  if curr_pat.connected:
     569                          key_dto = dto.firstnames + dto.lastnames + dto.dob.strftime('%Y-%m-%d') + dto.gender
     570                          names = curr_pat.get_active_name()
     571                          key_pat = names['firstnames'] + names['lastnames'] + curr_pat.get_formatted_dob(format = '%Y-%m-%d') + curr_pat['gender']
     572                          _log.debug('current patient: %s' % key_pat)
     573                          _log.debug('dto patient    : %s' % key_dto)
     574                          if key_dto == key_pat:
     575                                  gmDispatcher.send(signal='statustext', msg=_('The only external patient is already active in GNUmed.'), beep=False)
     576                                  return None
     577
     578          # one external person - look for internal match immediately ?
     579          if (len(dtos) == 1) and search_immediately:
     580                  dto = dtos[0]['dto']
     581
     582          # several external persons
     583          else:
     584                  if parent is None:
     585                          parent = wx.GetApp().GetTopWindow()
     586                  dlg = cSelectPersonDTOFromListDlg(parent=parent, id=-1)
     587                  dlg.set_dtos(dtos=dtos)
     588                  result = dlg.ShowModal()
     589                  if result == wx.ID_CANCEL:
     590                          return None
     591                  dto = dlg.get_selected_dto()['dto']
     592                  dlg.Destroy()
     593
     594          # search
     595          idents = dto.get_candidate_identities(can_create=True)
     596          if idents is None:
     597                  gmGuiHelpers.gm_show_info (_(
     598                          'Cannot create new patient:\n\n'
     599                          ' [%s %s (%s), %s]'
     600                          ) % (
     601                                  dto.firstnames, dto.lastnames, dto.gender, gmDateTime.pydt_strftime(dto.dob, '%Y %b %d')
     602                          ),
     603                          _('Activating external patient')
     604                  )
     605                  return None
     606
     607          if len(idents) == 1:
     608                  ident = idents[0]
     609
     610          if len(idents) > 1:
     611                  if parent is None:
     612                          parent = wx.GetApp().GetTopWindow()
     613                  dlg = cSelectPersonFromListDlg(parent=parent, id=-1)
     614                  dlg.set_persons(persons=idents)
     615                  result = dlg.ShowModal()
     616                  if result == wx.ID_CANCEL:
     617                          return None
     618                  ident = dlg.get_selected_person()
     619                  dlg.Destroy()
     620
     621          if activate_immediately:
     622                  if not set_active_patient(patient = ident):
     623                          gmGuiHelpers.gm_show_info (_(
     624                                  'Cannot activate patient:\n\n'
     625                                  '%s %s (%s)\n'
     626                                  '%s'
     627                                  ) % (
     628                                          dto.firstnames, dto.lastnames, dto.gender, gmDateTime.pydt_strftime(dto.dob, '%Y %b %d')
     629                                  ),
     630                                  _('Activating external patient')
     631                          )
     632                          return None
     633
     634          dto.import_extra_data(identity = ident)
     635          dto.delete_from_source()
     636
     637          return ident
     638  #============================================================
     639 -class cPersonSearchCtrl(wx.TextCtrl):
     640          """Widget for smart search for persons."""
     641
     642 -        def __init__(self, *args, **kwargs):
     643
     644                  try:
     645                          kwargs['style'] = kwargs['style'] | wx.TE_PROCESS_ENTER
     646                  except KeyError:
     647                          kwargs['style'] = wx.TE_PROCESS_ENTER
     648
     649                  # need to explicitly process ENTER events to avoid
     650                  # them being handed over to the next control
     651                  wx.TextCtrl.__init__(self, *args, **kwargs)
     652
     653                  self.person = None
     654
     655                  self._tt_search_hints = _(
     656                          'To search for a person, type any of:                   \n'
     657                          '\n'
     658                          ' - fragment(s) of last and/or first name(s)\n'
     659                          " - GNUmed ID of person (can start with '#')\n"
     660                          ' - any external ID of person\n'
     661                          " - date of birth (can start with '$' or '*')\n"
     662                          '\n'
     663                          'and hit <ENTER>.\n'
     664                          '\n'
     665                          'Shortcuts:\n'
     666                          ' <F2>\n'
     667                          '  - scan external sources for persons\n'
     668                          ' <CURSOR-UP>\n'
     669                          '  - recall most recently used search term\n'
     670                          ' <CURSOR-DOWN>\n'
     671                          '  - list 10 most recently found persons\n'
     672                  )
     673                  self.SetToolTipString(self._tt_search_hints)
     674
     675                  # FIXME: set query generator
     676                  self.__person_searcher = gmPersonSearch.cPatientSearcher_SQL()
     677
     678                  self._prev_search_term = None
     679                  self.__prev_idents = []
     680                  self._lclick_count = 0
     681
     682                  self.__register_events()
     683          #--------------------------------------------------------
     684          # properties
     685          #--------------------------------------------------------
     686 -        def _set_person(self, person):
     687                  self.__person = person
     688                  wx.CallAfter(self._display_name)
     689
     690 -        def _get_person(self):
     691                  return self.__person
     692
     693          person = property(_get_person, _set_person)
     694          #--------------------------------------------------------
     695          # utility methods
     696          #--------------------------------------------------------
     697 -        def _display_name(self):
     698                  name = u''
     699
     700                  if self.person is not None:
     701                          name = self.person['description']
     702
     703                  self.SetValue(name)
     704          #--------------------------------------------------------
     705 -        def _remember_ident(self, ident=None):
     706
     707                  if not isinstance(ident, gmPerson.cIdentity):
     708                          return False
     709
     710                  # only unique identities
     711                  for known_ident in self.__prev_idents:
     712                          if known_ident['pk_identity'] == ident['pk_identity']:
     713                                  return True
     714
     715                  self.__prev_idents.append(ident)
     716
     717                  # and only 10 of them
     718                  if len(self.__prev_idents) > 10:
     719                          self.__prev_idents.pop(0)
     720
     721                  return True
     722          #--------------------------------------------------------
     723          # event handling
     724          #--------------------------------------------------------
     725 -        def __register_events(self):
     726                  wx.EVT_CHAR(self, self.__on_char)
     727                  wx.EVT_SET_FOCUS(self, self._on_get_focus)
     728                  wx.EVT_KILL_FOCUS (self, self._on_loose_focus)
     729                  wx.EVT_TEXT_ENTER (self, self.GetId(), self.__on_enter)
     730          #--------------------------------------------------------
     731 -        def _on_get_focus(self, evt):
     732                  """upon tabbing in
     733
     734                  - select all text in the field so that the next
     735                    character typed will delete it
     736                  """
     737                  wx.CallAfter(self.SetSelection, -1, -1)
     738                  evt.Skip()
     739          #--------------------------------------------------------
     740 -        def _on_loose_focus(self, evt):
     741                  # - redraw the currently active name upon losing focus
     742                  #
     743                  # if we use wx.EVT_KILL_FOCUS we will also receive this event
     744                  # when closing our application or loosing focus to another
     745                  # application which is NOT what we intend to achieve,
     746                  # however, this is the least ugly way of doing this due to
     747                  # certain vagaries of wxPython (see the Wiki)
     748                  evt.Skip()
     749                  wx.CallAfter(self.__on_lost_focus)
     750          #--------------------------------------------------------
     751 -        def __on_lost_focus(self):
     752                  # just for good measure
     753                  self.SetSelection(0, 0)
     754                  self._display_name()
     755                  self._remember_ident(self.person)
     756          #--------------------------------------------------------
     757 -        def __on_char(self, evt):
     758                  self._on_char(evt)
     759
     760 -        def _on_char(self, evt):
     761                  """True: patient was selected.
     762                     False: no patient was selected.
     763                  """
     764                  keycode = evt.GetKeyCode()
     765
     766                  # list of previously active patients
     767                  if keycode == wx.WXK_DOWN:
     768                          evt.Skip()
     769                          if len(self.__prev_idents) == 0:
     770                                  return False
     771
     772                          dlg = cSelectPersonFromListDlg(parent = wx.GetTopLevelParent(self), id = -1)
     773                          dlg.set_persons(persons = self.__prev_idents)
     774                          result = dlg.ShowModal()
     775                          if result == wx.ID_OK:
     776                                  wx.BeginBusyCursor()
     777                                  self.person = dlg.get_selected_person()
     778                                  dlg.Destroy()
     779                                  wx.EndBusyCursor()
     780                                  return True
     781
     782                          dlg.Destroy()
     783                          return False
     784
     785                  # recall previous search fragment
     786                  if keycode == wx.WXK_UP:
     787                          evt.Skip()
     788                          # FIXME: cycling through previous fragments
     789                          if self._prev_search_term is not None:
     790                                  self.SetValue(self._prev_search_term)
     791                          return False
     792
     793                  # invoke external patient sources
     794                  if keycode == wx.WXK_F2:
     795                          evt.Skip()
     796                          dbcfg = gmCfg.cCfgSQL()
     797                          search_immediately = bool(dbcfg.get2 (
     798                                  option = 'patient_search.external_sources.immediately_search_if_single_source',
     799                                  workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
     800                                  bias = 'user',
     801                                  default = 0
     802                          ))
     803                          p = get_person_from_external_sources (
     804                                  parent = wx.GetTopLevelParent(self),
     805                                  search_immediately = search_immediately
     806                          )
     807                          if p is not None:
     808                                  self.person = p
     809                                  return True
     810                          return False
     811
     812                  # FIXME: invoke add new person
     813                  # FIXME: add popup menu apart from system one
     814
     815                  evt.Skip()
     816          #--------------------------------------------------------
     817 -        def __on_enter(self, evt):
     818                  """This is called from the ENTER handler."""
     819
     820                  # ENTER but no search term ?
     821                  curr_search_term = self.GetValue().strip()
     822                  if curr_search_term == '':
     823                          return None
     824
     825                  # same person anywys ?
     826                  if self.person is not None:
     827                          if curr_search_term == self.person['description']:
     828                                  return None
     829
     830                  # remember search fragment
     831                  if self.IsModified():
     832                          self._prev_search_term = curr_search_term
     833
     834                  self._on_enter(search_term = curr_search_term)
     835          #--------------------------------------------------------
     836 -        def _on_enter(self, search_term=None):
     837                  """This can be overridden in child classes."""
     838
     839                  wx.BeginBusyCursor()
     840
     841                  # get list of matching ids
     842                  idents = self.__person_searcher.get_identities(search_term)
     843
     844                  if idents is None:
     845                          wx.EndBusyCursor()
     846                          gmGuiHelpers.gm_show_info (
     847                                  _('Error searching for matching persons.\n\n'
     848                                    'Search term: "%s"'
     849                                  ) % search_term,
     850                                  _('selecting person')
     851                          )
     852                          return None
     853
     854                  _log.info("%s matching person(s) found", len(idents))
     855
     856                  if len(idents) == 0:
     857                          wx.EndBusyCursor()
     858
     859                          dlg = gmGuiHelpers.c2ButtonQuestionDlg (
     860                                  wx.GetTopLevelParent(self),
     861                                  -1,
     862                                  caption = _('Selecting patient'),
     863                                  question = _(
     864                                          'Cannot find any matching patients for the search term\n\n'
     865                                          ' "%s"\n\n'
     866                                          'You may want to try a shorter search term.\n'
     867                                  ) % search_term,
     868                                  button_defs = [
     869                                          {'label': _('Go back'), 'tooltip': _('Go back and search again.'), 'default': True},
     870                                          {'label': _('Create new'), 'tooltip': _('Create new patient.')}
     871                                  ]
     872                          )
     873                          if dlg.ShowModal() != wx.ID_NO:
     874                                  return
     875
     876                          success = create_new_person(activate = True)
     877                          if success:
     878                                  self.person = gmPerson.gmCurrentPatient()
     879                          else:
     880                                  self.person = None
     881                          return None
     882
     883                  # only one matching identity
     884                  if len(idents) == 1:
     885                          self.person = idents[0]
     886                          wx.EndBusyCursor()
     887                          return None
     888
     889                  # more than one matching identity: let user select from pick list
     890                  dlg = cSelectPersonFromListDlg(parent=wx.GetTopLevelParent(self), id=-1)
     891                  dlg.set_persons(persons=idents)
     892                  wx.EndBusyCursor()
     893                  result = dlg.ShowModal()
     894                  if result == wx.ID_CANCEL:
     895                          dlg.Destroy()
     896                          return None
     897
     898                  wx.BeginBusyCursor()
     899                  self.person = dlg.get_selected_person()
     900                  dlg.Destroy()
     901                  wx.EndBusyCursor()
     902
     903                  return None
     904  #============================================================
     905 -def _check_has_dob(patient=None):
     906
     907          if patient is None:
     908                  return
     909
     910          if patient['dob'] is None:
     911                  gmGuiHelpers.gm_show_warning (
     912                          aTitle = _('Checking date of birth'),
     913                          aMessage = _(
     914                                  '\n'
     915                                  ' %s\n'
     916                                  '\n'
     917                                  'The date of birth for this patient is not known !\n'
     918                                  '\n'
     919                                  'You can proceed to work on the patient but\n'
     920                                  'GNUmed will be unable to assist you with\n'
     921                                  'age-related decisions.\n'
     922                          ) % patient['description_gender']
     923                  )
     924
     925          return
     926  #------------------------------------------------------------
     927 -def _check_for_provider_chart_access(patient=None):
     928
     929          if patient is None:
     930                  return True
     931
     932          curr_prov = gmStaff.gmCurrentProvider()
     933
     934          # can view my own chart
     935          if patient.ID == curr_prov['pk_identity']:
     936                  return True
     937
     938          if patient.ID not in [ s['pk_identity'] for s in gmStaff.get_staff_list() ]:
     939                  return True
     940
     941          proceed = gmGuiHelpers.gm_show_question (
     942                  aTitle = _('Privacy check'),
     943                  aMessage = _(
     944                          'You have selected the chart of a member of staff,\n'
     945                          'for whom privacy is especially important:\n'
     946                          '\n'
     947                          '  %s, %s\n'
     948                          '\n'
     949                          'This may be OK depending on circumstances.\n'
     950                          '\n'
     951                          'Please be aware that accessing patient charts is\n'
     952                          'logged and that %s%s will be\n'
     953                          'notified of the access if you choose to proceed.\n'
     954                          '\n'
     955                          'Are you sure you want to draw this chart ?'
     956                  ) % (
     957                          patient.get_description_gender(),
     958                          patient.get_formatted_dob(),
     959                          gmTools.coalesce(patient['title'], u'', u'%s '),
     960                          patient['lastnames']
     961                  )
     962          )
     963
     964          if proceed:
     965                  prov = u'%s (%s%s %s)' % (
     966                          curr_prov['short_alias'],
     967                          gmTools.coalesce(curr_prov['title'], u'', u'%s '),
     968                          curr_prov['firstnames'],
     969                          curr_prov['lastnames']
     970                  )
     971                  pat = u'%s%s %s' % (
     972                          gmTools.coalesce(patient['title'], u'', u'%s '),
     973                          patient['firstnames'],
     974                          patient['lastnames']
     975                  )
     976                  # notify the staff member
     977                  gmProviderInbox.create_inbox_message (
     978                          staff = patient.staff_id,
     979                          message_type = _('Privacy notice'),
     980                          subject = _('Your chart has been accessed by %s.') % prov,
     981                          patient = patient.ID
     982                  )
     983                  # notify /me about the staff member notification
     984                  gmProviderInbox.create_inbox_message (
     985                          staff = curr_prov['pk_staff'],
     986                          message_type = _('Privacy notice'),
     987                          subject = _('Staff member %s has been notified of your chart access.') % pat
     988                  )
     989
     990          return proceed
     991  #------------------------------------------------------------
     992 -def _check_birthday(patient=None):
     993
     994          if patient['dob'] is None:
     995                  return
     996
     997          dbcfg = gmCfg.cCfgSQL()
     998          dob_distance = dbcfg.get2 (
     999                  option = u'patient_search.dob_warn_interval',
    1000                  workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    1001                  bias = u'user',
    1002                  default = u'1 week'
    1003          )
    1004
    1005          if not patient.dob_in_range(dob_distance, dob_distance):
    1006                  return
    1007
    1008          now = gmDateTime.pydt_now_here()
    1009          enc = gmI18N.get_encoding()
    1010          msg = _('%(pat)s turns %(age)s on %(month)s %(day)s ! (today is %(month_now)s %(day_now)s)') % {
    1011                  'pat': patient.get_description_gender(),
    1012                  'age': patient.get_medical_age().strip('y'),
    1013                  'month': patient.get_formatted_dob(format = '%B', encoding = enc),
    1014                  'day': patient.get_formatted_dob(format = '%d', encoding = enc),
    1015                  'month_now': gmDateTime.pydt_strftime(now, '%B', enc, gmDateTime.acc_months),
    1016                  'day_now': gmDateTime.pydt_strftime(now, '%d', enc, gmDateTime.acc_days)
    1017          }
    1018          gmDispatcher.send(signal = 'statustext', msg = msg)
    1019  #------------------------------------------------------------
    1020 -def set_active_patient(patient=None, forced_reload=False):
    1021
    1022          if isinstance(patient, gmPerson.cPatient):
    1023                  pass
    1024          elif isinstance(patient, gmPerson.cIdentity):
    1025                  patient = gmPerson.cPatient(aPK_obj = patient['pk_identity'])
    1026  #       elif isinstance(patient, cStaff):
    1027  #               patient = cPatient(aPK_obj=patient['pk_identity'])
    1028          elif isinstance(patient, gmPerson.gmCurrentPatient):
    1029                  patient = patient.patient
    1030          elif patient == -1:
    1031                  pass
    1032          else:
    1033                  # maybe integer ?
    1034                  success, pk = gmTools.input2int(initial = patient, minval = 1)
    1035                  if not success:
    1036                          raise ValueError('<patient> must be either -1, >0, or a cPatient, cIdentity or gmCurrentPatient instance, is: %s' % patient)
    1037                  # but also valid patient ID ?
    1038                  try:
    1039                          patient = gmPerson.cPatient(aPK_obj = pk)
    1040                  except:
    1041                          _log.exception('error changing active patient to [%s]' % patient)
    1042                          return False
    1043
    1044          _check_has_dob(patient = patient)
    1045
    1046          if not _check_for_provider_chart_access(patient = patient):
    1047                  return False
    1048
    1049          success = gmPerson.set_active_patient(patient = patient, forced_reload = forced_reload)
    1050
    1051          if not success:
    1052                  return False
    1053
    1054          _check_birthday(patient = patient)
    1055
    1056          return True
    1057  #------------------------------------------------------------
    1058 -class cActivePatientSelector(cPersonSearchCtrl):
    1059
    1060 -        def __init__ (self, *args, **kwargs):
    1061
    1062                  cPersonSearchCtrl.__init__(self, *args, **kwargs)
    1063
    1064                  # get configuration
    1065                  cfg = gmCfg.cCfgSQL()
    1066
    1067                  self.__always_dismiss_on_search = bool (
    1068                          cfg.get2 (
    1069                                  option = 'patient_search.always_dismiss_previous_patient',
    1070                                  workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    1071                                  bias = 'user',
    1072                                  default = 0
    1073                          )
    1074                  )
    1075
    1076                  self.__always_reload_after_search = bool (
    1077                          cfg.get2 (
    1078                                  option = 'patient_search.always_reload_new_patient',
    1079                                  workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
    1080                                  bias = 'user',
    1081                                  default = 0
    1082                          )
    1083                  )
    1084
    1085                  self.__register_events()
    1086          #--------------------------------------------------------
    1087          # utility methods
    1088          #--------------------------------------------------------
    1089 -        def _display_name(self):
    1090
    1091                  curr_pat = gmPerson.gmCurrentPatient()
    1092                  if curr_pat.connected:
    1093                          name = curr_pat['description']
    1094                          if curr_pat.locked:
    1095                                  name = _('%(name)s (locked)') % {'name': name}
    1096                  else:
    1097                          if curr_pat.locked:
    1098                                  name = _('<patient search locked>')
    1099                          else:
    1100                                  name = _('<type here to search patient>')
    1101
    1102                  self.SetValue(name)
    1103
    1104                  # adjust tooltip
    1105                  if self.person is None:
    1106                          self.SetToolTipString(self._tt_search_hints)
    1107                          return
    1108
    1109                  if (self.person['emergency_contact'] is None) and (self.person['comment'] is None):
    1110                          separator = u''
    1111                  else:
    1112                          separator = u'%s\n' % (gmTools.u_box_horiz_single * 40)
    1113
    1114                  tt = u'%s%s%s%s' % (
    1115                          gmTools.coalesce(self.person['emergency_contact'], u'', u'%s\n %%s\n' % _('In case of emergency contact:')),
    1116                          gmTools.coalesce(self.person['comment'], u'', u'\n%s\n'),
    1117                          separator,
    1118                          self._tt_search_hints
    1119                  )
    1120                  self.SetToolTipString(tt)
    1121          #--------------------------------------------------------
    1122 -        def _set_person_as_active_patient(self, pat):
    1123                  if not set_active_patient(patient=pat, forced_reload = self.__always_reload_after_search):
    1124                          _log.error('cannot change active patient')
    1125                          return None
    1126
    1127                  self._remember_ident(pat)
    1128
    1129                  return True
    1130          #--------------------------------------------------------
    1131          # event handling
    1132          #--------------------------------------------------------
    1133 -        def __register_events(self):
    1134                  # client internal signals
    1135                  gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
    1136                  gmDispatcher.connect(signal = u'name_mod_db', receiver = self._on_name_identity_change)
    1137                  gmDispatcher.connect(signal = u'identity_mod_db', receiver = self._on_name_identity_change)
    1138
    1139                  gmDispatcher.connect(signal = 'patient_locked', receiver = self._on_post_patient_selection)
    1140                  gmDispatcher.connect(signal = 'patient_unlocked', receiver = self._on_post_patient_selection)
    1141          #----------------------------------------------
    1142 -        def _on_name_identity_change(self, **kwargs):
    1143                  wx.CallAfter(self._display_name)
    1144          #----------------------------------------------
    1145 -        def _on_post_patient_selection(self, **kwargs):
    1146                  if gmPerson.gmCurrentPatient().connected:
    1147                          self.person = gmPerson.gmCurrentPatient().patient
    1148                  else:
    1149                          self.person = None
    1150          #----------------------------------------------
    1151 -        def _on_enter(self, search_term = None):
    1152
    1153                  if self.__always_dismiss_on_search:
    1154                          _log.warning("dismissing patient before patient search")
    1155                          self._set_person_as_active_patient(-1)
    1156
    1157                  super(self.__class__, self)._on_enter(search_term=search_term)
    1158
    1159                  if self.person is None:
    1160                          return
    1161
    1162                  self._set_person_as_active_patient(self.person)
    1163          #----------------------------------------------
    1164 -        def _on_char(self, evt):
    1165
    1166                  success = super(self.__class__, self)._on_char(evt)
    1167                  if success:
    1168                          self._set_person_as_active_patient(self.person)
    1169
    1170  #============================================================
    1171  # main
    1172  #------------------------------------------------------------
    1173  if __name__ == "__main__":
    1174
    1175          if len(sys.argv) > 1:
    1176                  if sys.argv[1] == 'test':
    1177                          gmI18N.activate_locale()
    1178                          gmI18N.install_domain()
    1179
    1180                          app = wx.PyWidgetTester(size = (200, 40))
    1181  #                       app.SetWidget(cSelectPersonFromListDlg, -1)
    1182                          app.SetWidget(cPersonSearchCtrl, -1)
    1183  #                       app.SetWidget(cActivePatientSelector, -1)
    1184                          app.MainLoop()
    1185
    1186  #============================================================
    1187  # docs
    1188  #------------------------------------------------------------
    1189  # functionality
    1190  # -------------
    1191  # - hitting ENTER on non-empty field (and more than threshold chars)
    1192  #   - start search
    1193  #   - display results in a list, prefixed with numbers
    1194  #   - last name
    1195  #   - first name
    1196  #   - gender
    1197  #   - age
    1198  #   - city + street (no ZIP, no number)
    1199  #   - last visit (highlighted if within a certain interval)
    1200  #   - arbitrary marker (e.g. office attendance this quartal, missing KVK, appointments, due dates)
    1201  #   - if none found -> go to entry of new patient
    1202  #   - scrolling in this list
    1203  #   - ENTER selects patient
    1204  #   - ESC cancels selection
    1205  #   - number selects patient
    1206  #
    1207  # - hitting cursor-up/-down
    1208  #   - cycle through history of last 10 search fragments
    1209  #
    1210  # - hitting alt-L = List, alt-P = previous
    1211  #   - show list of previous ten patients prefixed with numbers
    1212  #   - scrolling in list
    1213  #   - ENTER selects patient
    1214  #   - ESC cancels selection
    1215  #   - number selects patient
    1216  #
    1217  # - hitting ALT-N
    1218  #   - immediately goes to entry of new patient
    1219  #
    1220  # - hitting cursor-right in a patient selection list
    1221  #   - pops up more detail about the patient
    1222  #   - ESC/cursor-left goes back to list
    1223  #
    1224  # - hitting TAB
    1225  #   - makes sure the currently active patient is displayed
    1226
    1227  #------------------------------------------------------------
    1228  # samples
    1229  # -------
    1230  # working:
    1231  #  Ian Haywood
    1232  #  Haywood Ian
    1233  #  Haywood
    1234  #  Amador Jimenez (yes, two last names but no hyphen: Spain, for example)
    1235  #  Ian Haywood 19/12/1977
    1236  #  19/12/1977
    1237  #  19-12-1977
    1238  #  19.12.1977
    1239  #  19771219
    1240  #  $dob
    1241  #  *dob
    1242  #  #ID
    1243  #  ID
    1244  #  HIlbert, karsten
    1245  #  karsten, hilbert
    1246  #  kars, hilb
    1247  #
    1248  # non-working:
    1249  #  Haywood, Ian <40
    1250  #  ?, Ian 1977
    1251  #  Ian Haywood, 19/12/77
    1252  #  PUPIC
    1253  # "hilb; karsten, 23.10.74"
    1254
    1255  #------------------------------------------------------------
    1256  # notes
    1257  # -----
    1258  # >> 3. There are countries in which people have more than one
    1259  # >> (significant) lastname (spanish-speaking countries are one case :), some
    1260  # >> asian countries might be another one).
    1261  # -> we need per-country query generators ...
    1262
    1263  # search case sensitive by default, switch to insensitive if not found ?
    1264
    1265  # accent insensitive search:
    1266  #  select * from * where to_ascii(column, 'encoding') like '%test%';
    1267  # may not work with Unicode
    1268
    1269  # phrase wheel is most likely too slow
    1270
    1271  # extend search fragment history
    1272
    1273  # ask user whether to send off level 3 queries - or thread them
    1274
    1275  # we don't expect patient IDs in complicated patterns, hence any digits signify a date
    1276
    1277  # FIXME: make list window fit list size ...
    1278
    1279  # clear search field upon get-focus ?
    1280
    1281  # F1 -> context help with hotkey listing
    1282
    1283  # th -> th|t
    1284  # v/f/ph -> f|v|ph
    1285  # maybe don't do umlaut translation in the first 2-3 letters
    1286  # such that not to defeat index use for the first level query ?
    1287
    1288  # user defined function key to start search
    1289