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

Source Code for Module Gnumed.wxpython.gmPatSearchWidgets

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