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