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