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

Source Code for Module Gnumed.wxpython.gmPatSearchWidgets

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