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

Source Code for Module Gnumed.wxpython.gmPatSearchWidgets

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