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