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 " - date of birth (can start with '$' or '*')\n" 648 " - GNUmed ID of person (can start with '#')\n" 649 ' - external ID of person\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 gmProviderInbox.create_inbox_message ( 960 message_type = _('Privacy notice'), 961 subject = _('Your chart has been accessed by %s.') % prov, 962 patient = patient.ID, 963 staff = patient.staff_id 964 ) 965 pat = u'%s%s %s' % ( 966 gmTools.coalesce(patient['title'], u'', u'%s '), 967 patient['firstnames'], 968 patient['lastnames'] 969 ) 970 gmProviderInbox.create_inbox_message ( 971 message_type = _('Privacy notice'), 972 subject = _('Staff member %s has been notified of your chart access.') % pat, 973 patient = patient.ID, 974 staff = curr_prov['pk_staff'] 975 ) 976 977 return proceed
978 #------------------------------------------------------------
979 -def set_active_patient(patient=None, forced_reload=False):
980 981 _check_dob(patient = patient) 982 983 if not _check_for_provider_chart_access(patient = patient): 984 return False 985 986 success = gmPerson.set_active_patient(patient = patient, forced_reload = forced_reload) 987 988 if not success: 989 return False 990 991 if patient['dob'] is None: 992 return True 993 994 dbcfg = gmCfg.cCfgSQL() 995 dob_distance = dbcfg.get2 ( 996 option = u'patient_search.dob_warn_interval', 997 workplace = gmSurgery.gmCurrentPractice().active_workplace, 998 bias = u'user', 999 default = u'1 week' 1000 ) 1001 1002 if patient.dob_in_range(dob_distance, dob_distance): 1003 now = pyDT.datetime.now(tz = gmDateTime.gmCurrentLocalTimezone) 1004 enc = gmI18N.get_encoding() 1005 gmDispatcher.send(signal = 'statustext', msg = _( 1006 '%(pat)s turns %(age)s on %(month)s %(day)s ! (today is %(month_now)s %(day_now)s)') % { 1007 'pat': patient.get_description_gender(), 1008 'age': patient.get_medical_age().strip('y'), 1009 'month': patient.get_formatted_dob(format = '%B', encoding = enc), 1010 'day': patient.get_formatted_dob(format = '%d', encoding = enc), 1011 'month_now': now.strftime('%B').decode(enc), 1012 'day_now': now.strftime('%d') 1013 } 1014 ) 1015 1016 return True
1017 #------------------------------------------------------------
1018 -class cActivePatientSelector(cPersonSearchCtrl):
1019
1020 - def __init__ (self, *args, **kwargs):
1021 1022 cPersonSearchCtrl.__init__(self, *args, **kwargs) 1023 1024 # get configuration 1025 cfg = gmCfg.cCfgSQL() 1026 1027 self.__always_dismiss_on_search = bool ( 1028 cfg.get2 ( 1029 option = 'patient_search.always_dismiss_previous_patient', 1030 workplace = gmSurgery.gmCurrentPractice().active_workplace, 1031 bias = 'user', 1032 default = 0 1033 ) 1034 ) 1035 1036 self.__always_reload_after_search = bool ( 1037 cfg.get2 ( 1038 option = 'patient_search.always_reload_new_patient', 1039 workplace = gmSurgery.gmCurrentPractice().active_workplace, 1040 bias = 'user', 1041 default = 0 1042 ) 1043 ) 1044 1045 self.__register_events()
1046 #-------------------------------------------------------- 1047 # utility methods 1048 #--------------------------------------------------------
1049 - def _display_name(self):
1050 name = _('<type here to search patient>') 1051 1052 curr_pat = gmPerson.gmCurrentPatient() 1053 if curr_pat.connected: 1054 name = curr_pat['description'] 1055 if curr_pat.locked: 1056 name = _('%(name)s (locked)') % {'name': name} 1057 1058 self.SetValue(name) 1059 1060 if self.person is None: 1061 self.SetToolTipString(self._tt_search_hints) 1062 return 1063 1064 tt = u'%s%s-----------------------------------\n%s' % ( 1065 gmTools.coalesce(self.person['emergency_contact'], u'', _('In case of emergency contact:') + u'\n %s\n'), 1066 gmTools.coalesce(self.person['comment'], u'', u'\n%s\n'), 1067 self._tt_search_hints 1068 ) 1069 self.SetToolTipString(tt)
1070 #--------------------------------------------------------
1071 - def _set_person_as_active_patient(self, pat):
1072 if not set_active_patient(patient=pat, forced_reload = self.__always_reload_after_search): 1073 _log.error('cannot change active patient') 1074 return None 1075 1076 self._remember_ident(pat) 1077 1078 return True
1079 #-------------------------------------------------------- 1080 # event handling 1081 #--------------------------------------------------------
1082 - def __register_events(self):
1083 # client internal signals 1084 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection) 1085 gmDispatcher.connect(signal = u'name_mod_db', receiver = self._on_name_identity_change) 1086 gmDispatcher.connect(signal = u'identity_mod_db', receiver = self._on_name_identity_change) 1087 1088 gmDispatcher.connect(signal = 'patient_locked', receiver = self._on_post_patient_selection) 1089 gmDispatcher.connect(signal = 'patient_unlocked', receiver = self._on_post_patient_selection)
1090 #----------------------------------------------
1091 - def _on_name_identity_change(self, **kwargs):
1092 wx.CallAfter(self._display_name)
1093 #----------------------------------------------
1094 - def _on_post_patient_selection(self, **kwargs):
1095 if gmPerson.gmCurrentPatient().connected: 1096 self.person = gmPerson.gmCurrentPatient().patient 1097 else: 1098 self.person = None
1099 #----------------------------------------------
1100 - def _on_enter(self, search_term = None):
1101 1102 if self.__always_dismiss_on_search: 1103 _log.warning("dismissing patient before patient search") 1104 self._set_person_as_active_patient(-1) 1105 1106 super(self.__class__, self)._on_enter(search_term=search_term) 1107 1108 if self.person is None: 1109 return 1110 1111 self._set_person_as_active_patient(self.person)
1112 #----------------------------------------------
1113 - def _on_char(self, evt):
1114 1115 success = super(self.__class__, self)._on_char(evt) 1116 if success: 1117 self._set_person_as_active_patient(self.person)
1118 1119 #============================================================ 1120 # main 1121 #------------------------------------------------------------ 1122 if __name__ == "__main__": 1123 1124 if len(sys.argv) > 1: 1125 if sys.argv[1] == 'test': 1126 gmI18N.activate_locale() 1127 gmI18N.install_domain() 1128 1129 app = wx.PyWidgetTester(size = (200, 40)) 1130 # app.SetWidget(cSelectPersonFromListDlg, -1) 1131 app.SetWidget(cPersonSearchCtrl, -1) 1132 # app.SetWidget(cActivePatientSelector, -1) 1133 app.MainLoop() 1134 1135 #============================================================ 1136 # docs 1137 #------------------------------------------------------------ 1138 # functionality 1139 # ------------- 1140 # - hitting ENTER on non-empty field (and more than threshold chars) 1141 # - start search 1142 # - display results in a list, prefixed with numbers 1143 # - last name 1144 # - first name 1145 # - gender 1146 # - age 1147 # - city + street (no ZIP, no number) 1148 # - last visit (highlighted if within a certain interval) 1149 # - arbitrary marker (e.g. office attendance this quartal, missing KVK, appointments, due dates) 1150 # - if none found -> go to entry of new patient 1151 # - scrolling in this list 1152 # - ENTER selects patient 1153 # - ESC cancels selection 1154 # - number selects patient 1155 # 1156 # - hitting cursor-up/-down 1157 # - cycle through history of last 10 search fragments 1158 # 1159 # - hitting alt-L = List, alt-P = previous 1160 # - show list of previous ten patients prefixed with numbers 1161 # - scrolling in list 1162 # - ENTER selects patient 1163 # - ESC cancels selection 1164 # - number selects patient 1165 # 1166 # - hitting ALT-N 1167 # - immediately goes to entry of new patient 1168 # 1169 # - hitting cursor-right in a patient selection list 1170 # - pops up more detail about the patient 1171 # - ESC/cursor-left goes back to list 1172 # 1173 # - hitting TAB 1174 # - makes sure the currently active patient is displayed 1175 1176 #------------------------------------------------------------ 1177 # samples 1178 # ------- 1179 # working: 1180 # Ian Haywood 1181 # Haywood Ian 1182 # Haywood 1183 # Amador Jimenez (yes, two last names but no hyphen: Spain, for example) 1184 # Ian Haywood 19/12/1977 1185 # 19/12/1977 1186 # 19-12-1977 1187 # 19.12.1977 1188 # 19771219 1189 # $dob 1190 # *dob 1191 # #ID 1192 # ID 1193 # HIlbert, karsten 1194 # karsten, hilbert 1195 # kars, hilb 1196 # 1197 # non-working: 1198 # Haywood, Ian <40 1199 # ?, Ian 1977 1200 # Ian Haywood, 19/12/77 1201 # PUPIC 1202 # "hilb; karsten, 23.10.74" 1203 1204 #------------------------------------------------------------ 1205 # notes 1206 # ----- 1207 # >> 3. There are countries in which people have more than one 1208 # >> (significant) lastname (spanish-speaking countries are one case :), some 1209 # >> asian countries might be another one). 1210 # -> we need per-country query generators ... 1211 1212 # search case sensitive by default, switch to insensitive if not found ? 1213 1214 # accent insensitive search: 1215 # select * from * where to_ascii(column, 'encoding') like '%test%'; 1216 # may not work with Unicode 1217 1218 # phrase wheel is most likely too slow 1219 1220 # extend search fragment history 1221 1222 # ask user whether to send off level 3 queries - or thread them 1223 1224 # we don't expect patient IDs in complicated patterns, hence any digits signify a date 1225 1226 # FIXME: make list window fit list size ... 1227 1228 # clear search field upon get-focus ? 1229 1230 # F1 -> context help with hotkey listing 1231 1232 # th -> th|t 1233 # v/f/ph -> f|v|ph 1234 # maybe don't do umlaut translation in the first 2-3 letters 1235 # such that not to defeat index use for the first level query ? 1236 1237 # user defined function key to start search 1238