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

Source Code for Module Gnumed.wxpython.gmNarrativeWidgets

   1  """GNUmed narrative handling widgets.""" 
   2  #================================================================ 
   3  __version__ = "$Revision: 1.46 $" 
   4  __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>" 
   5   
   6  import sys, logging, os, os.path, time, re as regex, shutil 
   7   
   8   
   9  import wx 
  10  import wx.lib.expando as wx_expando 
  11  import wx.lib.agw.supertooltip as agw_stt 
  12  import wx.lib.statbmp as wx_genstatbmp 
  13   
  14   
  15  if __name__ == '__main__': 
  16          sys.path.insert(0, '../../') 
  17  from Gnumed.pycommon import gmI18N 
  18  from Gnumed.pycommon import gmDispatcher 
  19  from Gnumed.pycommon import gmTools 
  20  from Gnumed.pycommon import gmDateTime 
  21  from Gnumed.pycommon import gmShellAPI 
  22  from Gnumed.pycommon import gmPG2 
  23  from Gnumed.pycommon import gmCfg 
  24  from Gnumed.pycommon import gmMatchProvider 
  25   
  26  from Gnumed.business import gmPerson 
  27  from Gnumed.business import gmStaff 
  28  from Gnumed.business import gmEMRStructItems 
  29  from Gnumed.business import gmClinNarrative 
  30  from Gnumed.business import gmSurgery 
  31  from Gnumed.business import gmForms 
  32  from Gnumed.business import gmDocuments 
  33  from Gnumed.business import gmPersonSearch 
  34  from Gnumed.business import gmKeywordExpansion 
  35   
  36  from Gnumed.wxpython import gmListWidgets 
  37  from Gnumed.wxpython import gmEMRStructWidgets 
  38  from Gnumed.wxpython import gmRegetMixin 
  39  from Gnumed.wxpython import gmPhraseWheel 
  40  from Gnumed.wxpython import gmGuiHelpers 
  41  from Gnumed.wxpython import gmPatSearchWidgets 
  42  from Gnumed.wxpython import gmCfgWidgets 
  43  from Gnumed.wxpython import gmDocumentWidgets 
  44  from Gnumed.wxpython import gmKeywordExpansionWidgets 
  45   
  46  from Gnumed.exporters import gmPatientExporter 
  47   
  48   
  49  _log = logging.getLogger('gm.ui') 
  50  _log.info(__version__) 
  51  #============================================================ 
  52  # narrative related widgets/functions 
  53  #------------------------------------------------------------ 
54 -def move_progress_notes_to_another_encounter(parent=None, encounters=None, episodes=None, patient=None, move_all=False):
55 56 # sanity checks 57 if patient is None: 58 patient = gmPerson.gmCurrentPatient() 59 60 if not patient.connected: 61 gmDispatcher.send(signal = 'statustext', msg = _('Cannot move progress notes. No active patient.')) 62 return False 63 64 if parent is None: 65 parent = wx.GetApp().GetTopWindow() 66 67 emr = patient.get_emr() 68 69 if encounters is None: 70 encs = emr.get_encounters(episodes = episodes) 71 encounters = gmEMRStructWidgets.select_encounters ( 72 parent = parent, 73 patient = patient, 74 single_selection = False, 75 encounters = encs 76 ) 77 # cancelled 78 if encounters is None: 79 return True 80 # none selected 81 if len(encounters) == 0: 82 return True 83 84 notes = emr.get_clin_narrative ( 85 encounters = encounters, 86 episodes = episodes 87 ) 88 89 # which narrative 90 if move_all: 91 selected_narr = notes 92 else: 93 selected_narr = gmListWidgets.get_choices_from_list ( 94 parent = parent, 95 caption = _('Moving progress notes between encounters ...'), 96 single_selection = False, 97 can_return_empty = True, 98 data = notes, 99 msg = _('\n Select the progress notes to move from the list !\n\n'), 100 columns = [_('when'), _('who'), _('type'), _('entry')], 101 choices = [ 102 [ narr['date'].strftime('%x %H:%M'), 103 narr['provider'], 104 gmClinNarrative.soap_cat2l10n[narr['soap_cat']], 105 narr['narrative'].replace('\n', '/').replace('\r', '/') 106 ] for narr in notes 107 ] 108 ) 109 110 if not selected_narr: 111 return True 112 113 # which encounter to move to 114 enc2move2 = gmEMRStructWidgets.select_encounters ( 115 parent = parent, 116 patient = patient, 117 single_selection = True 118 ) 119 120 if not enc2move2: 121 return True 122 123 for narr in selected_narr: 124 narr['pk_encounter'] = enc2move2['pk_encounter'] 125 narr.save() 126 127 return True
128 #------------------------------------------------------------
129 -def manage_progress_notes(parent=None, encounters=None, episodes=None, patient=None):
130 131 # sanity checks 132 if patient is None: 133 patient = gmPerson.gmCurrentPatient() 134 135 if not patient.connected: 136 gmDispatcher.send(signal = 'statustext', msg = _('Cannot edit progress notes. No active patient.')) 137 return False 138 139 if parent is None: 140 parent = wx.GetApp().GetTopWindow() 141 142 emr = patient.get_emr() 143 #-------------------------- 144 def delete(item): 145 if item is None: 146 return False 147 dlg = gmGuiHelpers.c2ButtonQuestionDlg ( 148 parent, 149 -1, 150 caption = _('Deleting progress note'), 151 question = _( 152 'Are you positively sure you want to delete this\n' 153 'progress note from the medical record ?\n' 154 '\n' 155 'Note that even if you chose to delete the entry it will\n' 156 'still be (invisibly) kept in the audit trail to protect\n' 157 'you from litigation because physical deletion is known\n' 158 'to be unlawful in some jurisdictions.\n' 159 ), 160 button_defs = ( 161 {'label': _('Delete'), 'tooltip': _('Yes, delete the progress note.'), 'default': False}, 162 {'label': _('Cancel'), 'tooltip': _('No, do NOT delete the progress note.'), 'default': True} 163 ) 164 ) 165 decision = dlg.ShowModal() 166 167 if decision != wx.ID_YES: 168 return False 169 170 gmClinNarrative.delete_clin_narrative(narrative = item['pk_narrative']) 171 return True
172 #-------------------------- 173 def edit(item): 174 if item is None: 175 return False 176 177 dlg = gmGuiHelpers.cMultilineTextEntryDlg ( 178 parent, 179 -1, 180 title = _('Editing progress note'), 181 msg = _('This is the original progress note:'), 182 data = item.format(left_margin = u' ', fancy = True), 183 text = item['narrative'] 184 ) 185 decision = dlg.ShowModal() 186 187 if decision != wx.ID_SAVE: 188 return False 189 190 val = dlg.value 191 dlg.Destroy() 192 if val.strip() == u'': 193 return False 194 195 item['narrative'] = val 196 item.save_payload() 197 198 return True 199 #-------------------------- 200 def refresh(lctrl): 201 notes = emr.get_clin_narrative ( 202 encounters = encounters, 203 episodes = episodes, 204 providers = [ gmStaff.gmCurrentProvider()['short_alias'] ] 205 ) 206 lctrl.set_string_items(items = [ 207 [ narr['date'].strftime('%x %H:%M'), 208 gmClinNarrative.soap_cat2l10n[narr['soap_cat']], 209 narr['narrative'].replace('\n', '/').replace('\r', '/') 210 ] for narr in notes 211 ]) 212 lctrl.set_data(data = notes) 213 #-------------------------- 214 215 gmListWidgets.get_choices_from_list ( 216 parent = parent, 217 caption = _('Managing progress notes'), 218 msg = _( 219 '\n' 220 ' This list shows the progress notes by %s.\n' 221 '\n' 222 ) % gmStaff.gmCurrentProvider()['short_alias'], 223 columns = [_('when'), _('type'), _('entry')], 224 single_selection = True, 225 can_return_empty = False, 226 edit_callback = edit, 227 delete_callback = delete, 228 refresh_callback = refresh 229 ) 230 #------------------------------------------------------------
231 -def search_narrative_across_emrs(parent=None):
232 233 if parent is None: 234 parent = wx.GetApp().GetTopWindow() 235 236 searcher = wx.TextEntryDialog ( 237 parent = parent, 238 message = _('Enter (regex) term to search for across all EMRs:'), 239 caption = _('Text search across all EMRs'), 240 style = wx.OK | wx.CANCEL | wx.CENTRE 241 ) 242 result = searcher.ShowModal() 243 244 if result != wx.ID_OK: 245 return 246 247 wx.BeginBusyCursor() 248 term = searcher.GetValue() 249 searcher.Destroy() 250 results = gmClinNarrative.search_text_across_emrs(search_term = term) 251 wx.EndBusyCursor() 252 253 if len(results) == 0: 254 gmGuiHelpers.gm_show_info ( 255 _( 256 'Nothing found for search term:\n' 257 ' "%s"' 258 ) % term, 259 _('Search results') 260 ) 261 return 262 263 items = [ [gmPerson.cIdentity(aPK_obj = 264 r['pk_patient'])['description_gender'], r['narrative'], 265 r['src_table']] for r in results ] 266 267 selected_patient = gmListWidgets.get_choices_from_list ( 268 parent = parent, 269 caption = _('Search results for %s') % term, 270 choices = items, 271 columns = [_('Patient'), _('Match'), _('Match location')], 272 data = [ r['pk_patient'] for r in results ], 273 single_selection = True, 274 can_return_empty = False 275 ) 276 277 if selected_patient is None: 278 return 279 280 wx.CallAfter(gmPatSearchWidgets.set_active_patient, patient = gmPerson.cIdentity(aPK_obj = selected_patient))
281 #------------------------------------------------------------
282 -def search_narrative_in_emr(parent=None, patient=None):
283 284 # sanity checks 285 if patient is None: 286 patient = gmPerson.gmCurrentPatient() 287 288 if not patient.connected: 289 gmDispatcher.send(signal = 'statustext', msg = _('Cannot search EMR. No active patient.')) 290 return False 291 292 if parent is None: 293 parent = wx.GetApp().GetTopWindow() 294 295 searcher = wx.TextEntryDialog ( 296 parent = parent, 297 message = _('Enter search term:'), 298 caption = _('Text search of entire EMR of active patient'), 299 style = wx.OK | wx.CANCEL | wx.CENTRE 300 ) 301 result = searcher.ShowModal() 302 303 if result != wx.ID_OK: 304 searcher.Destroy() 305 return False 306 307 wx.BeginBusyCursor() 308 val = searcher.GetValue() 309 searcher.Destroy() 310 emr = patient.get_emr() 311 rows = emr.search_narrative_simple(val) 312 wx.EndBusyCursor() 313 314 if len(rows) == 0: 315 gmGuiHelpers.gm_show_info ( 316 _( 317 'Nothing found for search term:\n' 318 ' "%s"' 319 ) % val, 320 _('Search results') 321 ) 322 return True 323 324 txt = u'' 325 for row in rows: 326 txt += u'%s: %s\n' % ( 327 row['soap_cat'], 328 row['narrative'] 329 ) 330 331 txt += u' %s: %s - %s %s\n' % ( 332 _('Encounter'), 333 row['encounter_started'].strftime('%x %H:%M'), 334 row['encounter_ended'].strftime('%H:%M'), 335 row['encounter_type'] 336 ) 337 txt += u' %s: %s\n' % ( 338 _('Episode'), 339 row['episode'] 340 ) 341 txt += u' %s: %s\n\n' % ( 342 _('Health issue'), 343 row['health_issue'] 344 ) 345 346 msg = _( 347 'Search term was: "%s"\n' 348 '\n' 349 'Search results:\n\n' 350 '%s\n' 351 ) % (val, txt) 352 353 dlg = wx.MessageDialog ( 354 parent = parent, 355 message = msg, 356 caption = _('Search results for %s') % val, 357 style = wx.OK | wx.STAY_ON_TOP 358 ) 359 dlg.ShowModal() 360 dlg.Destroy() 361 362 return True
363 #------------------------------------------------------------
364 -def export_narrative_for_medistar_import(parent=None, soap_cats=u'soapu', encounter=None):
365 366 # sanity checks 367 pat = gmPerson.gmCurrentPatient() 368 if not pat.connected: 369 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR for Medistar. No active patient.')) 370 return False 371 372 if encounter is None: 373 encounter = pat.get_emr().active_encounter 374 375 if parent is None: 376 parent = wx.GetApp().GetTopWindow() 377 378 # get file name 379 aWildcard = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files")) 380 # FIXME: make configurable 381 aDefDir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed','export'))) 382 # FIXME: make configurable 383 fname = '%s-%s-%s-%s-%s.txt' % ( 384 'Medistar-MD', 385 time.strftime('%Y-%m-%d',time.localtime()), 386 pat['lastnames'].replace(' ', '-'), 387 pat['firstnames'].replace(' ', '_'), 388 pat.get_formatted_dob(format = '%Y-%m-%d') 389 ) 390 dlg = wx.FileDialog ( 391 parent = parent, 392 message = _("Save EMR extract for MEDISTAR import as..."), 393 defaultDir = aDefDir, 394 defaultFile = fname, 395 wildcard = aWildcard, 396 style = wx.SAVE 397 ) 398 choice = dlg.ShowModal() 399 fname = dlg.GetPath() 400 dlg.Destroy() 401 if choice != wx.ID_OK: 402 return False 403 404 wx.BeginBusyCursor() 405 _log.debug('exporting encounter for medistar import to [%s]', fname) 406 exporter = gmPatientExporter.cMedistarSOAPExporter() 407 successful, fname = exporter.export_to_file ( 408 filename = fname, 409 encounter = encounter, 410 soap_cats = u'soapu', 411 export_to_import_file = True 412 ) 413 if not successful: 414 gmGuiHelpers.gm_show_error ( 415 _('Error exporting progress notes for MEDISTAR import.'), 416 _('MEDISTAR progress notes export') 417 ) 418 wx.EndBusyCursor() 419 return False 420 421 gmDispatcher.send(signal = 'statustext', msg = _('Successfully exported progress notes into file [%s] for Medistar import.') % fname, beep=False) 422 423 wx.EndBusyCursor() 424 return True
425 #------------------------------------------------------------
426 -def select_narrative_from_episodes_new(parent=None, soap_cats=None):
427 """soap_cats needs to be a list""" 428 429 if parent is None: 430 parent = wx.GetApp().GetTopWindow() 431 432 pat = gmPerson.gmCurrentPatient() 433 emr = pat.get_emr() 434 435 selected_soap = {} 436 selected_narrative_pks = [] 437 438 #----------------------------------------------- 439 def pick_soap_from_episode(episode): 440 441 narr_for_epi = emr.get_clin_narrative(episodes = [episode['pk_episode']], soap_cats = soap_cats) 442 443 if len(narr_for_epi) == 0: 444 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episode.')) 445 return True 446 447 dlg = cNarrativeListSelectorDlg ( 448 parent = parent, 449 id = -1, 450 narrative = narr_for_epi, 451 msg = _( 452 '\n This is the narrative (type %s) for the chosen episodes.\n' 453 '\n' 454 ' Now, mark the entries you want to include in your report.\n' 455 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soapu')) ]) 456 ) 457 # selection_idxs = [] 458 # for idx in range(len(narr_for_epi)): 459 # if narr_for_epi[idx]['pk_narrative'] in selected_narrative_pks: 460 # selection_idxs.append(idx) 461 # if len(selection_idxs) != 0: 462 # dlg.set_selections(selections = selection_idxs) 463 btn_pressed = dlg.ShowModal() 464 selected_narr = dlg.get_selected_item_data() 465 dlg.Destroy() 466 467 if btn_pressed == wx.ID_CANCEL: 468 return True 469 470 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ] 471 for narr in selected_narr: 472 selected_soap[narr['pk_narrative']] = narr 473 474 print "before returning from picking soap" 475 476 return True
477 #----------------------------------------------- 478 selected_episode_pks = [] 479 480 all_epis = [ epi for epi in emr.get_episodes() if epi.has_narrative ] 481 482 if len(all_epis) == 0: 483 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.')) 484 return [] 485 486 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg ( 487 parent = parent, 488 id = -1, 489 episodes = all_epis, 490 msg = _('\n Select the the episode you want to report on.\n') 491 ) 492 # selection_idxs = [] 493 # for idx in range(len(all_epis)): 494 # if all_epis[idx]['pk_episode'] in selected_episode_pks: 495 # selection_idxs.append(idx) 496 # if len(selection_idxs) != 0: 497 # dlg.set_selections(selections = selection_idxs) 498 dlg.left_extra_button = ( 499 _('Pick SOAP'), 500 _('Pick SOAP entries from topmost selected episode'), 501 pick_soap_from_episode 502 ) 503 btn_pressed = dlg.ShowModal() 504 dlg.Destroy() 505 506 if btn_pressed == wx.ID_CANCEL: 507 return None 508 509 return selected_soap.values() 510 #------------------------------------------------------------
511 -def select_narrative_from_episodes(parent=None, soap_cats=None):
512 """soap_cats needs to be a list""" 513 514 pat = gmPerson.gmCurrentPatient() 515 emr = pat.get_emr() 516 517 if parent is None: 518 parent = wx.GetApp().GetTopWindow() 519 520 selected_soap = {} 521 selected_issue_pks = [] 522 selected_episode_pks = [] 523 selected_narrative_pks = [] 524 525 while 1: 526 # 1) select health issues to select episodes from 527 all_issues = emr.get_health_issues() 528 all_issues.insert(0, gmEMRStructItems.get_dummy_health_issue()) 529 dlg = gmEMRStructWidgets.cIssueListSelectorDlg ( 530 parent = parent, 531 id = -1, 532 issues = all_issues, 533 msg = _('\n In the list below mark the health issues you want to report on.\n') 534 ) 535 selection_idxs = [] 536 for idx in range(len(all_issues)): 537 if all_issues[idx]['pk_health_issue'] in selected_issue_pks: 538 selection_idxs.append(idx) 539 if len(selection_idxs) != 0: 540 dlg.set_selections(selections = selection_idxs) 541 btn_pressed = dlg.ShowModal() 542 selected_issues = dlg.get_selected_item_data() 543 dlg.Destroy() 544 545 if btn_pressed == wx.ID_CANCEL: 546 return selected_soap.values() 547 548 selected_issue_pks = [ i['pk_health_issue'] for i in selected_issues ] 549 550 while 1: 551 # 2) select episodes to select items from 552 all_epis = emr.get_episodes(issues = selected_issue_pks) 553 554 if len(all_epis) == 0: 555 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.')) 556 break 557 558 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg ( 559 parent = parent, 560 id = -1, 561 episodes = all_epis, 562 msg = _( 563 '\n These are the episodes known for the health issues just selected.\n\n' 564 ' Now, mark the the episodes you want to report on.\n' 565 ) 566 ) 567 selection_idxs = [] 568 for idx in range(len(all_epis)): 569 if all_epis[idx]['pk_episode'] in selected_episode_pks: 570 selection_idxs.append(idx) 571 if len(selection_idxs) != 0: 572 dlg.set_selections(selections = selection_idxs) 573 btn_pressed = dlg.ShowModal() 574 selected_epis = dlg.get_selected_item_data() 575 dlg.Destroy() 576 577 if btn_pressed == wx.ID_CANCEL: 578 break 579 580 selected_episode_pks = [ i['pk_episode'] for i in selected_epis ] 581 582 # 3) select narrative corresponding to the above constraints 583 all_narr = emr.get_clin_narrative(episodes = selected_episode_pks, soap_cats = soap_cats) 584 585 if len(all_narr) == 0: 586 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episodes.')) 587 continue 588 589 dlg = cNarrativeListSelectorDlg ( 590 parent = parent, 591 id = -1, 592 narrative = all_narr, 593 msg = _( 594 '\n This is the narrative (type %s) for the chosen episodes.\n\n' 595 ' Now, mark the entries you want to include in your report.\n' 596 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soapu')) ]) 597 ) 598 selection_idxs = [] 599 for idx in range(len(all_narr)): 600 if all_narr[idx]['pk_narrative'] in selected_narrative_pks: 601 selection_idxs.append(idx) 602 if len(selection_idxs) != 0: 603 dlg.set_selections(selections = selection_idxs) 604 btn_pressed = dlg.ShowModal() 605 selected_narr = dlg.get_selected_item_data() 606 dlg.Destroy() 607 608 if btn_pressed == wx.ID_CANCEL: 609 continue 610 611 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ] 612 for narr in selected_narr: 613 selected_soap[narr['pk_narrative']] = narr
614 #------------------------------------------------------------
615 -class cNarrativeListSelectorDlg(gmListWidgets.cGenericListSelectorDlg):
616
617 - def __init__(self, *args, **kwargs):
618 619 narrative = kwargs['narrative'] 620 del kwargs['narrative'] 621 622 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs) 623 624 self.SetTitle(_('Select the narrative you are interested in ...')) 625 # FIXME: add epi/issue 626 self._LCTRL_items.set_columns([_('when'), _('who'), _('type'), _('entry')]) #, _('Episode'), u'', _('Health Issue')]) 627 # FIXME: date used should be date of encounter, not date_modified 628 self._LCTRL_items.set_string_items ( 629 items = [ [narr['date'].strftime('%x %H:%M'), narr['provider'], gmClinNarrative.soap_cat2l10n[narr['soap_cat']], narr['narrative'].replace('\n', '/').replace('\r', '/')] for narr in narrative ] 630 ) 631 self._LCTRL_items.set_column_widths() 632 self._LCTRL_items.set_data(data = narrative)
633 #------------------------------------------------------------ 634 from Gnumed.wxGladeWidgets import wxgMoveNarrativeDlg 635
636 -class cMoveNarrativeDlg(wxgMoveNarrativeDlg.wxgMoveNarrativeDlg):
637
638 - def __init__(self, *args, **kwargs):
639 640 self.encounter = kwargs['encounter'] 641 self.source_episode = kwargs['episode'] 642 del kwargs['encounter'] 643 del kwargs['episode'] 644 645 wxgMoveNarrativeDlg.wxgMoveNarrativeDlg.__init__(self, *args, **kwargs) 646 647 self.LBL_source_episode.SetLabel(u'%s%s' % (self.source_episode['description'], gmTools.coalesce(self.source_episode['health_issue'], u'', u' (%s)'))) 648 self.LBL_encounter.SetLabel('%s: %s %s - %s' % ( 649 self.encounter['started'].strftime('%x').decode(gmI18N.get_encoding()), 650 self.encounter['l10n_type'], 651 self.encounter['started'].strftime('%H:%M'), 652 self.encounter['last_affirmed'].strftime('%H:%M') 653 )) 654 pat = gmPerson.gmCurrentPatient() 655 emr = pat.get_emr() 656 narr = emr.get_clin_narrative(episodes=[self.source_episode['pk_episode']], encounters=[self.encounter['pk_encounter']]) 657 if len(narr) == 0: 658 narr = [{'narrative': _('There is no narrative for this episode in this encounter.')}] 659 self.LBL_narrative.SetLabel(u'\n'.join([n['narrative'] for n in narr]))
660 661 #------------------------------------------------------------
662 - def _on_move_button_pressed(self, event):
663 664 target_episode = self._PRW_episode_selector.GetData(can_create = False) 665 666 if target_episode is None: 667 gmDispatcher.send(signal='statustext', msg=_('Must select episode to move narrative to first.')) 668 # FIXME: set to pink 669 self._PRW_episode_selector.SetFocus() 670 return False 671 672 target_episode = gmEMRStructItems.cEpisode(aPK_obj=target_episode) 673 674 self.encounter.transfer_clinical_data ( 675 source_episode = self.source_episode, 676 target_episode = target_episode 677 ) 678 679 if self.IsModal(): 680 self.EndModal(wx.ID_OK) 681 else: 682 self.Close()
683 #============================================================ 684 from Gnumed.wxGladeWidgets import wxgSoapPluginPnl 685
686 -class cSoapPluginPnl(wxgSoapPluginPnl.wxgSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
687 """A panel for in-context editing of progress notes. 688 689 Expects to be used as a notebook page. 690 691 Left hand side: 692 - problem list (health issues and active episodes) 693 - previous notes 694 695 Right hand side: 696 - encounter details fields 697 - notebook with progress note editors 698 - visual progress notes 699 - hints 700 701 Listens to patient change signals, thus acts on the current patient. 702 """
703 - def __init__(self, *args, **kwargs):
704 705 wxgSoapPluginPnl.wxgSoapPluginPnl.__init__(self, *args, **kwargs) 706 gmRegetMixin.cRegetOnPaintMixin.__init__(self) 707 708 self.__pat = gmPerson.gmCurrentPatient() 709 self.__patient_just_changed = False 710 self.__init_ui() 711 self.__reset_ui_content() 712 713 self.__register_interests()
714 #-------------------------------------------------------- 715 # public API 716 #--------------------------------------------------------
717 - def save_encounter(self):
718 719 if not self.__encounter_valid_for_save(): 720 return False 721 722 emr = self.__pat.get_emr() 723 enc = emr.active_encounter 724 725 rfe = self._TCTRL_rfe.GetValue().strip() 726 if len(rfe) == 0: 727 enc['reason_for_encounter'] = None 728 else: 729 enc['reason_for_encounter'] = rfe 730 aoe = self._TCTRL_aoe.GetValue().strip() 731 if len(aoe) == 0: 732 enc['assessment_of_encounter'] = None 733 else: 734 enc['assessment_of_encounter'] = aoe 735 736 enc.save_payload() 737 738 enc.generic_codes_rfe = [ c['data'] for c in self._PRW_rfe_codes.GetData() ] 739 enc.generic_codes_aoe = [ c['data'] for c in self._PRW_aoe_codes.GetData() ] 740 741 return True
742 #-------------------------------------------------------- 743 # internal helpers 744 #--------------------------------------------------------
745 - def __init_ui(self):
746 self._LCTRL_active_problems.set_columns([_('Last'), _('Problem'), _('In health issue')]) 747 self._LCTRL_active_problems.set_string_items() 748 749 self._splitter_main.SetSashGravity(0.5) 750 self._splitter_left.SetSashGravity(0.5) 751 752 splitter_size = self._splitter_main.GetSizeTuple()[0] 753 self._splitter_main.SetSashPosition(splitter_size * 3 / 10, True) 754 755 splitter_size = self._splitter_left.GetSizeTuple()[1] 756 self._splitter_left.SetSashPosition(splitter_size * 6 / 20, True) 757 758 self._NB_soap_editors.DeleteAllPages() 759 self._NB_soap_editors.MoveAfterInTabOrder(self._PRW_aoe_codes)
760 #--------------------------------------------------------
762 start = self._PRW_encounter_start.GetData() 763 if start is None: 764 return 765 start = start.get_pydt() 766 767 end = self._PRW_encounter_end.GetData() 768 if end is None: 769 fts = gmDateTime.cFuzzyTimestamp ( 770 timestamp = start, 771 accuracy = gmDateTime.acc_minutes 772 ) 773 self._PRW_encounter_end.SetText(fts.format_accurately(), data = fts) 774 return 775 end = end.get_pydt() 776 777 if start > end: 778 end = end.replace ( 779 year = start.year, 780 month = start.month, 781 day = start.day 782 ) 783 fts = gmDateTime.cFuzzyTimestamp ( 784 timestamp = end, 785 accuracy = gmDateTime.acc_minutes 786 ) 787 self._PRW_encounter_end.SetText(fts.format_accurately(), data = fts) 788 return 789 790 emr = self.__pat.get_emr() 791 if start != emr.active_encounter['started']: 792 end = end.replace ( 793 year = start.year, 794 month = start.month, 795 day = start.day 796 ) 797 fts = gmDateTime.cFuzzyTimestamp ( 798 timestamp = end, 799 accuracy = gmDateTime.acc_minutes 800 ) 801 self._PRW_encounter_end.SetText(fts.format_accurately(), data = fts) 802 return 803 804 return
805 #--------------------------------------------------------
806 - def __reset_ui_content(self):
807 """Clear all information from input panel.""" 808 809 self._LCTRL_active_problems.set_string_items() 810 811 self._TCTRL_recent_notes.SetValue(u'') 812 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on selected problem')) 813 814 self._TCTRL_rfe.SetValue(u'') 815 self._PRW_rfe_codes.SetText(suppress_smarts = True) 816 self._TCTRL_aoe.SetValue(u'') 817 self._PRW_aoe_codes.SetText(suppress_smarts = True) 818 819 self._NB_soap_editors.DeleteAllPages() 820 self._NB_soap_editors.add_editor()
821 #--------------------------------------------------------
822 - def __refresh_problem_list(self):
823 """Update health problems list.""" 824 825 self._LCTRL_active_problems.set_string_items() 826 827 emr = self.__pat.get_emr() 828 problems = emr.get_problems ( 829 include_closed_episodes = self._CHBOX_show_closed_episodes.IsChecked(), 830 include_irrelevant_issues = self._CHBOX_irrelevant_issues.IsChecked() 831 ) 832 833 list_items = [] 834 active_problems = [] 835 for problem in problems: 836 if not problem['problem_active']: 837 if not problem['is_potential_problem']: 838 continue 839 840 active_problems.append(problem) 841 842 if problem['type'] == 'issue': 843 issue = emr.problem2issue(problem) 844 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue']) 845 if last_encounter is None: 846 last = issue['modified_when'].strftime('%m/%Y') 847 else: 848 last = last_encounter['last_affirmed'].strftime('%m/%Y') 849 850 list_items.append([last, problem['problem'], gmTools.u_left_arrow_with_tail]) 851 852 elif problem['type'] == 'episode': 853 epi = emr.problem2episode(problem) 854 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode']) 855 if last_encounter is None: 856 last = epi['episode_modified_when'].strftime('%m/%Y') 857 else: 858 last = last_encounter['last_affirmed'].strftime('%m/%Y') 859 860 list_items.append ([ 861 last, 862 problem['problem'], 863 gmTools.coalesce(initial = epi['health_issue'], instead = u'?') #gmTools.u_diameter 864 ]) 865 866 self._LCTRL_active_problems.set_string_items(items = list_items) 867 self._LCTRL_active_problems.set_column_widths() 868 self._LCTRL_active_problems.set_data(data = active_problems) 869 870 showing_potential_problems = ( 871 self._CHBOX_show_closed_episodes.IsChecked() 872 or 873 self._CHBOX_irrelevant_issues.IsChecked() 874 ) 875 if showing_potential_problems: 876 self._SZR_problem_list_staticbox.SetLabel(_('%s (active+potential) problems') % len(list_items)) 877 else: 878 self._SZR_problem_list_staticbox.SetLabel(_('%s active problems') % len(list_items)) 879 880 return True
881 #--------------------------------------------------------
882 - def __get_soap_for_issue_problem(self, problem=None):
883 soap = u'' 884 emr = self.__pat.get_emr() 885 prev_enc = emr.get_last_but_one_encounter(issue_id = problem['pk_health_issue']) 886 if prev_enc is not None: 887 soap += prev_enc.format ( 888 issues = [ problem['pk_health_issue'] ], 889 with_soap = True, 890 with_docs = False, 891 with_tests = False, 892 patient = self.__pat, 893 fancy_header = False, 894 with_rfe_aoe = True 895 ) 896 897 tmp = emr.active_encounter.format_soap ( 898 soap_cats = 'soapu', 899 emr = emr, 900 issues = [ problem['pk_health_issue'] ], 901 ) 902 if len(tmp) > 0: 903 soap += _('Current encounter:') + u'\n' 904 soap += u'\n'.join(tmp) + u'\n' 905 906 if problem['summary'] is not None: 907 soap += u'\n-- %s ----------\n%s' % ( 908 _('Cumulative summary'), 909 gmTools.wrap ( 910 text = problem['summary'], 911 width = 45, 912 initial_indent = u' ', 913 subsequent_indent = u' ' 914 ).strip('\n') 915 ) 916 917 return soap
918 #--------------------------------------------------------
919 - def __get_soap_for_episode_problem(self, problem=None):
920 soap = u'' 921 emr = self.__pat.get_emr() 922 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_episode']) 923 if prev_enc is not None: 924 soap += prev_enc.format ( 925 episodes = [ problem['pk_episode'] ], 926 with_soap = True, 927 with_docs = False, 928 with_tests = False, 929 patient = self.__pat, 930 fancy_header = False, 931 with_rfe_aoe = True 932 ) 933 else: 934 if problem['pk_health_issue'] is not None: 935 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_health_issue']) 936 if prev_enc is not None: 937 soap += prev_enc.format ( 938 with_soap = True, 939 with_docs = False, 940 with_tests = False, 941 patient = self.__pat, 942 issues = [ problem['pk_health_issue'] ], 943 fancy_header = False, 944 with_rfe_aoe = True 945 ) 946 947 tmp = emr.active_encounter.format_soap ( 948 soap_cats = 'soapu', 949 emr = emr, 950 issues = [ problem['pk_health_issue'] ], 951 ) 952 if len(tmp) > 0: 953 soap += _('Current encounter:') + u'\n' 954 soap += u'\n'.join(tmp) + u'\n' 955 956 if problem['summary'] is not None: 957 soap += u'\n-- %s ----------\n%s' % ( 958 _('Cumulative summary'), 959 gmTools.wrap ( 960 text = problem['summary'], 961 width = 45, 962 initial_indent = u' ', 963 subsequent_indent = u' ' 964 ).strip('\n') 965 ) 966 967 return soap
968 #--------------------------------------------------------
969 - def __refresh_current_editor(self):
970 self._NB_soap_editors.refresh_current_editor()
971 #--------------------------------------------------------
973 if not self.__patient_just_changed: 974 return 975 976 dbcfg = gmCfg.cCfgSQL() 977 auto_open_recent_problems = bool(dbcfg.get2 ( 978 option = u'horstspace.soap_editor.auto_open_latest_episodes', 979 workplace = gmSurgery.gmCurrentPractice().active_workplace, 980 bias = u'user', 981 default = True 982 )) 983 984 self.__patient_just_changed = False 985 emr = self.__pat.get_emr() 986 recent_epis = emr.active_encounter.get_episodes() 987 prev_enc = emr.get_last_but_one_encounter() 988 if prev_enc is not None: 989 recent_epis.extend(prev_enc.get_episodes()) 990 991 for epi in recent_epis: 992 if not epi['episode_open']: 993 continue 994 self._NB_soap_editors.add_editor(problem = epi, allow_same_problem = False)
995 #--------------------------------------------------------
996 - def __refresh_recent_notes(self, problem=None):
997 """This refreshes the recent-notes part.""" 998 999 soap = u'' 1000 caption = u'<?>' 1001 1002 if problem['type'] == u'issue': 1003 caption = problem['problem'][:35] 1004 soap = self.__get_soap_for_issue_problem(problem = problem) 1005 1006 elif problem['type'] == u'episode': 1007 caption = problem['problem'][:35] 1008 soap = self.__get_soap_for_episode_problem(problem = problem) 1009 1010 self._TCTRL_recent_notes.SetValue(soap) 1011 self._TCTRL_recent_notes.ShowPosition(self._TCTRL_recent_notes.GetLastPosition()) 1012 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on %s%s%s') % ( 1013 gmTools.u_left_double_angle_quote, 1014 caption, 1015 gmTools.u_right_double_angle_quote 1016 )) 1017 1018 self._TCTRL_recent_notes.Refresh() 1019 1020 return True
1021 #--------------------------------------------------------
1022 - def __refresh_encounter(self):
1023 """Update encounter fields.""" 1024 1025 emr = self.__pat.get_emr() 1026 enc = emr.active_encounter 1027 1028 self._TCTRL_rfe.SetValue(gmTools.coalesce(enc['reason_for_encounter'], u'')) 1029 val, data = self._PRW_rfe_codes.generic_linked_codes2item_dict(enc.generic_codes_rfe) 1030 self._PRW_rfe_codes.SetText(val, data) 1031 1032 self._TCTRL_aoe.SetValue(gmTools.coalesce(enc['assessment_of_encounter'], u'')) 1033 val, data = self._PRW_aoe_codes.generic_linked_codes2item_dict(enc.generic_codes_aoe) 1034 self._PRW_aoe_codes.SetText(val, data) 1035 1036 self._TCTRL_rfe.Refresh() 1037 self._PRW_rfe_codes.Refresh() 1038 self._TCTRL_aoe.Refresh() 1039 self._PRW_aoe_codes.Refresh()
1040 #--------------------------------------------------------
1041 - def __encounter_modified(self):
1042 """Assumes that the field data is valid.""" 1043 1044 emr = self.__pat.get_emr() 1045 enc = emr.active_encounter 1046 1047 data = { 1048 'pk_type': enc['pk_type'], 1049 'reason_for_encounter': gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u''), 1050 'assessment_of_encounter': gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''), 1051 'pk_location': enc['pk_location'], 1052 'pk_patient': enc['pk_patient'], 1053 'pk_generic_codes_rfe': self._PRW_rfe_codes.GetData(), 1054 'pk_generic_codes_aoe': self._PRW_aoe_codes.GetData(), 1055 'started': enc['started'], 1056 'last_affirmed': enc['last_affirmed'] 1057 } 1058 1059 return not enc.same_payload(another_object = data)
1060 #--------------------------------------------------------
1061 - def __encounter_valid_for_save(self):
1062 return True
1063 #-------------------------------------------------------- 1064 # event handling 1065 #--------------------------------------------------------
1066 - def __register_interests(self):
1067 """Configure enabled event signals.""" 1068 # client internal signals 1069 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection) 1070 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection) 1071 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db) 1072 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db) 1073 gmDispatcher.connect(signal = u'episode_code_mod_db', receiver = self._on_episode_issue_mod_db) 1074 gmDispatcher.connect(signal = u'doc_mod_db', receiver = self._on_doc_mod_db) # visual progress notes 1075 gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified) 1076 gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_switched) 1077 gmDispatcher.connect(signal = u'rfe_code_mod_db', receiver = self._on_encounter_code_modified) 1078 gmDispatcher.connect(signal = u'aoe_code_mod_db', receiver = self._on_encounter_code_modified) 1079 1080 # synchronous signals 1081 self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback) 1082 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
1083 #--------------------------------------------------------
1084 - def _pre_selection_callback(self):
1085 """Another patient is about to be activated. 1086 1087 Patient change will not proceed before this returns True. 1088 """ 1089 # don't worry about the encounter here - it will be offered 1090 # for editing higher up if anything was saved to the EMR 1091 if not self.__pat.connected: 1092 return True 1093 return self._NB_soap_editors.warn_on_unsaved_soap()
1094 #--------------------------------------------------------
1095 - def _pre_exit_callback(self):
1096 """The client is about to be shut down. 1097 1098 Shutdown will not proceed before this returns. 1099 """ 1100 if not self.__pat.connected: 1101 return True 1102 1103 # if self.__encounter_modified(): 1104 # do_save_enc = gmGuiHelpers.gm_show_question ( 1105 # aMessage = _( 1106 # 'You have modified the details\n' 1107 # 'of the current encounter.\n' 1108 # '\n' 1109 # 'Do you want to save those changes ?' 1110 # ), 1111 # aTitle = _('Starting new encounter') 1112 # ) 1113 # if do_save_enc: 1114 # if not self.save_encounter(): 1115 # gmDispatcher.send(signal = u'statustext', msg = _('Error saving current encounter.'), beep = True) 1116 1117 emr = self.__pat.get_emr() 1118 saved = self._NB_soap_editors.save_all_editors ( 1119 emr = emr, 1120 episode_name_candidates = [ 1121 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''), 1122 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'') 1123 ] 1124 ) 1125 if not saved: 1126 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True) 1127 return True
1128 #--------------------------------------------------------
1129 - def _on_pre_patient_selection(self):
1130 wx.CallAfter(self.__on_pre_patient_selection)
1131 #--------------------------------------------------------
1132 - def __on_pre_patient_selection(self):
1133 self.__reset_ui_content()
1134 #--------------------------------------------------------
1135 - def _on_post_patient_selection(self):
1136 wx.CallAfter(self._schedule_data_reget) 1137 self.__patient_just_changed = True
1138 #--------------------------------------------------------
1139 - def _on_doc_mod_db(self):
1140 wx.CallAfter(self.__refresh_current_editor)
1141 #--------------------------------------------------------
1142 - def _on_episode_issue_mod_db(self):
1143 wx.CallAfter(self._schedule_data_reget)
1144 #--------------------------------------------------------
1146 emr = self.__pat.get_emr() 1147 emr.active_encounter.refetch_payload() 1148 wx.CallAfter(self.__refresh_encounter)
1149 #--------------------------------------------------------
1151 wx.CallAfter(self.__refresh_encounter)
1152 #--------------------------------------------------------
1154 wx.CallAfter(self.__on_current_encounter_switched)
1155 #--------------------------------------------------------
1157 self.__refresh_encounter()
1158 #-------------------------------------------------------- 1159 # problem list specific events 1160 #--------------------------------------------------------
1161 - def _on_problem_focused(self, event):
1162 """Show related note at the bottom.""" 1163 pass
1164 #--------------------------------------------------------
1165 - def _on_problem_rclick(self, event):
1166 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True) 1167 if problem['type'] == u'issue': 1168 gmEMRStructWidgets.edit_health_issue(parent = self, issue = problem.get_as_health_issue()) 1169 return 1170 1171 if problem['type'] == u'episode': 1172 gmEMRStructWidgets.edit_episode(parent = self, episode = problem.get_as_episode()) 1173 return 1174 1175 event.Skip()
1176 #--------------------------------------------------------
1177 - def _on_problem_selected(self, event):
1178 """Show related note at the bottom.""" 1179 emr = self.__pat.get_emr() 1180 self.__refresh_recent_notes ( 1181 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True) 1182 )
1183 #--------------------------------------------------------
1184 - def _on_problem_activated(self, event):
1185 """Open progress note editor for this problem. 1186 """ 1187 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True) 1188 if problem is None: 1189 return True 1190 1191 dbcfg = gmCfg.cCfgSQL() 1192 allow_duplicate_editors = bool(dbcfg.get2 ( 1193 option = u'horstspace.soap_editor.allow_same_episode_multiple_times', 1194 workplace = gmSurgery.gmCurrentPractice().active_workplace, 1195 bias = u'user', 1196 default = False 1197 )) 1198 if self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors): 1199 return True 1200 1201 gmGuiHelpers.gm_show_error ( 1202 aMessage = _( 1203 'Cannot open progress note editor for\n\n' 1204 '[%s].\n\n' 1205 ) % problem['problem'], 1206 aTitle = _('opening progress note editor') 1207 ) 1208 event.Skip() 1209 return False
1210 #--------------------------------------------------------
1211 - def _on_show_closed_episodes_checked(self, event):
1212 self.__refresh_problem_list()
1213 #--------------------------------------------------------
1214 - def _on_irrelevant_issues_checked(self, event):
1215 self.__refresh_problem_list()
1216 #-------------------------------------------------------- 1217 # SOAP editor specific buttons 1218 #--------------------------------------------------------
1219 - def _on_discard_editor_button_pressed(self, event):
1220 self._NB_soap_editors.close_current_editor() 1221 event.Skip()
1222 #--------------------------------------------------------
1223 - def _on_new_editor_button_pressed(self, event):
1224 self._NB_soap_editors.add_editor() 1225 event.Skip()
1226 #--------------------------------------------------------
1227 - def _on_clear_editor_button_pressed(self, event):
1228 self._NB_soap_editors.clear_current_editor() 1229 event.Skip()
1230 #--------------------------------------------------------
1231 - def _on_save_note_button_pressed(self, event):
1232 emr = self.__pat.get_emr() 1233 self._NB_soap_editors.save_current_editor ( 1234 emr = emr, 1235 episode_name_candidates = [ 1236 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''), 1237 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'') 1238 ] 1239 ) 1240 event.Skip()
1241 #--------------------------------------------------------
1242 - def _on_save_note_under_button_pressed(self, event):
1243 encounter = gmEMRStructWidgets.select_encounters ( 1244 parent = self, 1245 patient = self.__pat, 1246 single_selection = True 1247 ) 1248 # cancelled or None selected: 1249 if encounter is None: 1250 return 1251 1252 emr = self.__pat.get_emr() 1253 self._NB_soap_editors.save_current_editor ( 1254 emr = emr, 1255 encounter = encounter['pk_encounter'], 1256 episode_name_candidates = [ 1257 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''), 1258 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'') 1259 ] 1260 ) 1261 event.Skip()
1262 #--------------------------------------------------------
1263 - def _on_image_button_pressed(self, event):
1264 emr = self.__pat.get_emr() 1265 self._NB_soap_editors.add_visual_progress_note_to_current_problem() 1266 event.Skip()
1267 #-------------------------------------------------------- 1268 # encounter specific buttons 1269 #--------------------------------------------------------
1270 - def _on_save_encounter_button_pressed(self, event):
1271 self.save_encounter() 1272 event.Skip()
1273 #-------------------------------------------------------- 1274 # other buttons 1275 #--------------------------------------------------------
1276 - def _on_save_all_button_pressed(self, event):
1277 self.save_encounter() 1278 time.sleep(0.3) 1279 event.Skip() 1280 wx.SafeYield() 1281 1282 wx.CallAfter(self._save_all_button_pressed_bottom_half) 1283 wx.SafeYield()
1284 #--------------------------------------------------------
1286 emr = self.__pat.get_emr() 1287 saved = self._NB_soap_editors.save_all_editors ( 1288 emr = emr, 1289 episode_name_candidates = [ 1290 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''), 1291 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'') 1292 ] 1293 ) 1294 if not saved: 1295 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
1296 #-------------------------------------------------------- 1297 # reget mixin API 1298 #--------------------------------------------------------
1299 - def _populate_with_data(self):
1300 self.__refresh_problem_list() 1301 self.__refresh_encounter() 1302 self.__setup_initial_patient_editors() 1303 return True
1304 #============================================================
1305 -class cSoapNoteInputNotebook(wx.Notebook):
1306 """A notebook holding panels with progress note editors. 1307 1308 There can be one or several progress note editor panel 1309 for each episode being worked on. The editor class in 1310 each panel is configurable. 1311 1312 There will always be one open editor. 1313 """
1314 - def __init__(self, *args, **kwargs):
1315 1316 kwargs['style'] = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER 1317 1318 wx.Notebook.__init__(self, *args, **kwargs)
1319 #-------------------------------------------------------- 1320 # public API 1321 #--------------------------------------------------------
1322 - def add_editor(self, problem=None, allow_same_problem=False):
1323 """Add a progress note editor page. 1324 1325 The way <allow_same_problem> is currently used in callers 1326 it only applies to unassociated episodes. 1327 """ 1328 problem_to_add = problem 1329 1330 # determine label 1331 if problem_to_add is None: 1332 label = _('new problem') 1333 else: 1334 # normalize problem type 1335 if isinstance(problem_to_add, gmEMRStructItems.cEpisode): 1336 problem_to_add = gmEMRStructItems.episode2problem(episode = problem_to_add) 1337 1338 elif isinstance(problem_to_add, gmEMRStructItems.cHealthIssue): 1339 problem_to_add = gmEMRStructItems.health_issue2problem(episode = problem_to_add) 1340 1341 if not isinstance(problem_to_add, gmEMRStructItems.cProblem): 1342 raise TypeError('cannot open progress note editor for [%s]' % problem_to_add) 1343 1344 label = problem_to_add['problem'] 1345 # FIXME: configure maximum length 1346 if len(label) > 23: 1347 label = label[:21] + gmTools.u_ellipsis 1348 1349 # new unassociated problem or dupes allowed 1350 if (problem_to_add is None) or allow_same_problem: 1351 new_page = cSoapNoteExpandoEditAreaPnl(parent = self, id = -1, problem = problem_to_add) 1352 result = self.AddPage ( 1353 page = new_page, 1354 text = label, 1355 select = True 1356 ) 1357 return result 1358 1359 # real problem, no dupes allowed 1360 # - raise existing editor 1361 for page_idx in range(self.GetPageCount()): 1362 page = self.GetPage(page_idx) 1363 1364 # editor is for unassociated new problem 1365 if page.problem is None: 1366 continue 1367 1368 # editor is for episode 1369 if page.problem['type'] == 'episode': 1370 if page.problem['pk_episode'] == problem_to_add['pk_episode']: 1371 self.SetSelection(page_idx) 1372 gmDispatcher.send(signal = u'statustext', msg = u'Raising existing editor.', beep = True) 1373 return True 1374 continue 1375 1376 # editor is for health issue 1377 if page.problem['type'] == 'issue': 1378 if page.problem['pk_health_issue'] == problem_to_add['pk_health_issue']: 1379 self.SetSelection(page_idx) 1380 gmDispatcher.send(signal = u'statustext', msg = u'Raising existing editor.', beep = True) 1381 return True 1382 continue 1383 1384 # - or add new editor 1385 new_page = cSoapNoteExpandoEditAreaPnl(parent = self, id = -1, problem = problem_to_add) 1386 result = self.AddPage ( 1387 page = new_page, 1388 text = label, 1389 select = True 1390 ) 1391 1392 return result
1393 #--------------------------------------------------------
1394 - def close_current_editor(self):
1395 1396 page_idx = self.GetSelection() 1397 page = self.GetPage(page_idx) 1398 1399 if not page.empty: 1400 really_discard = gmGuiHelpers.gm_show_question ( 1401 _('Are you sure you really want to\n' 1402 'discard this progress note ?\n' 1403 ), 1404 _('Discarding progress note') 1405 ) 1406 if really_discard is False: 1407 return 1408 1409 self.DeletePage(page_idx) 1410 1411 # always keep one unassociated editor open 1412 if self.GetPageCount() == 0: 1413 self.add_editor()
1414 #--------------------------------------------------------
1415 - def save_current_editor(self, emr=None, episode_name_candidates=None, encounter=None):
1416 1417 page_idx = self.GetSelection() 1418 page = self.GetPage(page_idx) 1419 1420 if not page.save(emr = emr, episode_name_candidates = episode_name_candidates, encounter = encounter): 1421 return 1422 1423 self.DeletePage(page_idx) 1424 1425 # always keep one unassociated editor open 1426 if self.GetPageCount() == 0: 1427 self.add_editor()
1428 #--------------------------------------------------------
1429 - def warn_on_unsaved_soap(self):
1430 for page_idx in range(self.GetPageCount()): 1431 page = self.GetPage(page_idx) 1432 if page.empty: 1433 continue 1434 1435 gmGuiHelpers.gm_show_warning ( 1436 _('There are unsaved progress notes !\n'), 1437 _('Unsaved progress notes') 1438 ) 1439 return False 1440 1441 return True
1442 #--------------------------------------------------------
1443 - def save_all_editors(self, emr=None, episode_name_candidates=None):
1444 1445 _log.debug('saving editors: %s', self.GetPageCount()) 1446 1447 all_closed = True 1448 for page_idx in range((self.GetPageCount() - 1), -1, -1): 1449 _log.debug('#%s of %s', page_idx, self.GetPageCount()) 1450 try: 1451 self.ChangeSelection(page_idx) 1452 _log.debug('editor raised') 1453 except: 1454 _log.exception('cannot raise editor') 1455 page = self.GetPage(page_idx) 1456 if page.save(emr = emr, episode_name_candidates = episode_name_candidates): 1457 _log.debug('saved, deleting now') 1458 self.DeletePage(page_idx) 1459 else: 1460 _log.debug('not saved, not deleting') 1461 all_closed = False 1462 1463 # always keep one unassociated editor open 1464 if self.GetPageCount() == 0: 1465 self.add_editor() 1466 1467 return (all_closed is True)
1468 #--------------------------------------------------------
1469 - def clear_current_editor(self):
1470 page_idx = self.GetSelection() 1471 page = self.GetPage(page_idx) 1472 page.clear()
1473 #--------------------------------------------------------
1474 - def get_current_problem(self):
1475 page_idx = self.GetSelection() 1476 page = self.GetPage(page_idx) 1477 return page.problem
1478 #--------------------------------------------------------
1479 - def refresh_current_editor(self):
1480 page_idx = self.GetSelection() 1481 page = self.GetPage(page_idx) 1482 page.refresh()
1483 #--------------------------------------------------------
1485 page_idx = self.GetSelection() 1486 page = self.GetPage(page_idx) 1487 page.add_visual_progress_note()
1488 #============================================================ 1489 from Gnumed.wxGladeWidgets import wxgSoapNoteExpandoEditAreaPnl 1490
1491 -class cSoapNoteExpandoEditAreaPnl(wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl):
1492 """An Edit Area like panel for entering progress notes. 1493 1494 Subjective: Codes: 1495 expando text ctrl 1496 Objective: Codes: 1497 expando text ctrl 1498 Assessment: Codes: 1499 expando text ctrl 1500 Plan: Codes: 1501 expando text ctrl 1502 visual progress notes 1503 panel with images 1504 Episode synopsis: Codes: 1505 text ctrl 1506 1507 - knows the problem this edit area is about 1508 - can deal with issue or episode type problems 1509 """ 1510
1511 - def __init__(self, *args, **kwargs):
1512 1513 try: 1514 self.problem = kwargs['problem'] 1515 del kwargs['problem'] 1516 except KeyError: 1517 self.problem = None 1518 1519 wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl.__init__(self, *args, **kwargs) 1520 1521 self.soap_fields = [ 1522 self._TCTRL_Soap, 1523 self._TCTRL_sOap, 1524 self._TCTRL_soAp, 1525 self._TCTRL_soaP 1526 ] 1527 1528 self.__init_ui() 1529 self.__register_interests()
1530 #--------------------------------------------------------
1531 - def __init_ui(self):
1532 self.refresh_summary() 1533 if self.problem is not None: 1534 if self.problem['summary'] is None: 1535 self._TCTRL_episode_summary.SetValue(u'') 1536 self.refresh_visual_soap()
1537 #--------------------------------------------------------
1538 - def refresh(self):
1539 self.refresh_summary() 1540 self.refresh_visual_soap()
1541 #--------------------------------------------------------
1542 - def refresh_summary(self):
1543 self._TCTRL_episode_summary.SetValue(u'') 1544 self._PRW_episode_codes.SetText(u'', self._PRW_episode_codes.list2data_dict([])) 1545 self._LBL_summary.SetLabel(_('Episode synopsis')) 1546 1547 # new problem ? 1548 if self.problem is None: 1549 return 1550 1551 # issue-level problem ? 1552 if self.problem['type'] == u'issue': 1553 return 1554 1555 # episode-level problem 1556 caption = _(u'Synopsis (%s)') % ( 1557 gmDateTime.pydt_strftime ( 1558 self.problem['modified_when'], 1559 format = '%B %Y', 1560 accuracy = gmDateTime.acc_days 1561 ) 1562 ) 1563 self._LBL_summary.SetLabel(caption) 1564 1565 if self.problem['summary'] is not None: 1566 self._TCTRL_episode_summary.SetValue(self.problem['summary'].strip()) 1567 1568 val, data = self._PRW_episode_codes.generic_linked_codes2item_dict(self.problem.generic_codes) 1569 self._PRW_episode_codes.SetText(val, data)
1570 #--------------------------------------------------------
1571 - def refresh_visual_soap(self):
1572 if self.problem is None: 1573 self._PNL_visual_soap.refresh(document_folder = None) 1574 return 1575 1576 if self.problem['type'] == u'issue': 1577 self._PNL_visual_soap.refresh(document_folder = None) 1578 return 1579 1580 if self.problem['type'] == u'episode': 1581 pat = gmPerson.gmCurrentPatient() 1582 doc_folder = pat.get_document_folder() 1583 emr = pat.get_emr() 1584 self._PNL_visual_soap.refresh ( 1585 document_folder = doc_folder, 1586 episodes = [self.problem['pk_episode']], 1587 encounter = emr.active_encounter['pk_encounter'] 1588 ) 1589 return
1590 #--------------------------------------------------------
1591 - def clear(self):
1592 for field in self.soap_fields: 1593 field.SetValue(u'') 1594 self._TCTRL_episode_summary.SetValue(u'') 1595 self._LBL_summary.SetLabel(_('Episode synopsis')) 1596 self._PRW_episode_codes.SetText(u'', self._PRW_episode_codes.list2data_dict([])) 1597 self._PNL_visual_soap.clear()
1598 #--------------------------------------------------------
1599 - def add_visual_progress_note(self):
1600 fname, discard_unmodified = select_visual_progress_note_template(parent = self) 1601 if fname is None: 1602 return False 1603 1604 if self.problem is None: 1605 issue = None 1606 episode = None 1607 elif self.problem['type'] == 'issue': 1608 issue = self.problem['pk_health_issue'] 1609 episode = None 1610 else: 1611 issue = self.problem['pk_health_issue'] 1612 episode = gmEMRStructItems.problem2episode(self.problem) 1613 1614 wx.CallAfter ( 1615 edit_visual_progress_note, 1616 filename = fname, 1617 episode = episode, 1618 discard_unmodified = discard_unmodified, 1619 health_issue = issue 1620 )
1621 #--------------------------------------------------------
1622 - def save(self, emr=None, episode_name_candidates=None, encounter=None):
1623 1624 if self.empty: 1625 return True 1626 1627 # new episode (standalone=unassociated or new-in-issue) 1628 if (self.problem is None) or (self.problem['type'] == 'issue'): 1629 episode = self.__create_new_episode(emr = emr, episode_name_candidates = episode_name_candidates) 1630 # user cancelled 1631 if episode is None: 1632 return False 1633 # existing episode 1634 else: 1635 episode = emr.problem2episode(self.problem) 1636 1637 if encounter is None: 1638 encounter = emr.current_encounter['pk_encounter'] 1639 1640 soap_notes = [] 1641 for note in self.soap: 1642 saved, data = gmClinNarrative.create_clin_narrative ( 1643 soap_cat = note[0], 1644 narrative = note[1], 1645 episode_id = episode['pk_episode'], 1646 encounter_id = encounter 1647 ) 1648 if saved: 1649 soap_notes.append(data) 1650 1651 # codes per narrative ! 1652 # for note in soap_notes: 1653 # if note['soap_cat'] == u's': 1654 # codes = self._PRW_Soap_codes 1655 # elif note['soap_cat'] == u'o': 1656 # elif note['soap_cat'] == u'a': 1657 # elif note['soap_cat'] == u'p': 1658 1659 # set summary but only if not already set above for a 1660 # newly created episode (either standalone or within 1661 # a health issue) 1662 if self.problem is not None: 1663 if self.problem['type'] == 'episode': 1664 episode['summary'] = self._TCTRL_episode_summary.GetValue().strip() 1665 episode.save() 1666 1667 # codes for episode 1668 episode.generic_codes = [ d['data'] for d in self._PRW_episode_codes.GetData() ] 1669 1670 return True
1671 #-------------------------------------------------------- 1672 # internal helpers 1673 #--------------------------------------------------------
1674 - def __create_new_episode(self, emr=None, episode_name_candidates=None):
1675 1676 episode_name_candidates.append(self._TCTRL_episode_summary.GetValue().strip()) 1677 for candidate in episode_name_candidates: 1678 if candidate is None: 1679 continue 1680 epi_name = candidate.strip().replace('\r', '//').replace('\n', '//') 1681 break 1682 1683 dlg = wx.TextEntryDialog ( 1684 parent = self, 1685 message = _('Enter a short working name for this new problem:'), 1686 caption = _('Creating a problem (episode) to save the notelet under ...'), 1687 defaultValue = epi_name, 1688 style = wx.OK | wx.CANCEL | wx.CENTRE 1689 ) 1690 decision = dlg.ShowModal() 1691 if decision != wx.ID_OK: 1692 return None 1693 1694 epi_name = dlg.GetValue().strip() 1695 if epi_name == u'': 1696 gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note')) 1697 return None 1698 1699 # create episode 1700 new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True) 1701 new_episode['summary'] = self._TCTRL_episode_summary.GetValue().strip() 1702 new_episode.save() 1703 1704 if self.problem is not None: 1705 issue = emr.problem2issue(self.problem) 1706 if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True): 1707 gmGuiHelpers.gm_show_warning ( 1708 _( 1709 'The new episode:\n' 1710 '\n' 1711 ' "%s"\n' 1712 '\n' 1713 'will remain unassociated despite the editor\n' 1714 'having been invoked from the health issue:\n' 1715 '\n' 1716 ' "%s"' 1717 ) % ( 1718 new_episode['description'], 1719 issue['description'] 1720 ), 1721 _('saving progress note') 1722 ) 1723 1724 return new_episode
1725 #-------------------------------------------------------- 1726 # event handling 1727 #--------------------------------------------------------
1728 - def __register_interests(self):
1729 for field in self.soap_fields: 1730 wx_expando.EVT_ETC_LAYOUT_NEEDED(field, field.GetId(), self._on_expando_needs_layout) 1731 wx_expando.EVT_ETC_LAYOUT_NEEDED(self._TCTRL_episode_summary, self._TCTRL_episode_summary.GetId(), self._on_expando_needs_layout) 1732 gmDispatcher.connect(signal = u'doc_page_mod_db', receiver = self._refresh_visual_soap)
1733 #--------------------------------------------------------
1734 - def _refresh_visual_soap(self):
1735 wx.CallAfter(self.refresh_visual_soap)
1736 #--------------------------------------------------------
1737 - def _on_expando_needs_layout(self, evt):
1738 # need to tell ourselves to re-Layout to refresh scroll bars 1739 1740 # provoke adding scrollbar if needed 1741 #self.Fit() # works on Linux but not on Windows 1742 self.FitInside() # needed on Windows rather than self.Fit() 1743 1744 if self.HasScrollbar(wx.VERTICAL): 1745 # scroll panel to show cursor 1746 expando = self.FindWindowById(evt.GetId()) 1747 y_expando = expando.GetPositionTuple()[1] 1748 h_expando = expando.GetSizeTuple()[1] 1749 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1 1750 if expando.NumberOfLines == 0: 1751 no_of_lines = 1 1752 else: 1753 no_of_lines = expando.NumberOfLines 1754 y_cursor = int(round((float(line_cursor) / no_of_lines) * h_expando)) 1755 y_desired_visible = y_expando + y_cursor 1756 1757 y_view = self.ViewStart[1] 1758 h_view = self.GetClientSizeTuple()[1] 1759 1760 # print "expando:", y_expando, "->", h_expando, ", lines:", expando.NumberOfLines 1761 # print "cursor :", y_cursor, "at line", line_cursor, ", insertion point:", expando.GetInsertionPoint() 1762 # print "wanted :", y_desired_visible 1763 # print "view-y :", y_view 1764 # print "scroll2:", h_view 1765 1766 # expando starts before view 1767 if y_desired_visible < y_view: 1768 # print "need to scroll up" 1769 self.Scroll(0, y_desired_visible) 1770 1771 if y_desired_visible > h_view: 1772 # print "need to scroll down" 1773 self.Scroll(0, y_desired_visible)
1774 #-------------------------------------------------------- 1775 # properties 1776 #--------------------------------------------------------
1777 - def _get_soap(self):
1778 soap_notes = [] 1779 1780 tmp = self._TCTRL_Soap.GetValue().strip() 1781 if tmp != u'': 1782 soap_notes.append(['s', tmp]) 1783 1784 tmp = self._TCTRL_sOap.GetValue().strip() 1785 if tmp != u'': 1786 soap_notes.append(['o', tmp]) 1787 1788 tmp = self._TCTRL_soAp.GetValue().strip() 1789 if tmp != u'': 1790 soap_notes.append(['a', tmp]) 1791 1792 tmp = self._TCTRL_soaP.GetValue().strip() 1793 if tmp != u'': 1794 soap_notes.append(['p', tmp]) 1795 1796 return soap_notes
1797 1798 soap = property(_get_soap, lambda x:x) 1799 #--------------------------------------------------------
1800 - def _get_empty(self):
1801 1802 # soap fields 1803 for field in self.soap_fields: 1804 if field.GetValue().strip() != u'': 1805 return False 1806 1807 # summary 1808 summary = self._TCTRL_episode_summary.GetValue().strip() 1809 if self.problem is None: 1810 if summary != u'': 1811 return False 1812 elif self.problem['type'] == u'issue': 1813 if summary != u'': 1814 return False 1815 else: 1816 if self.problem['summary'] is None: 1817 if summary != u'': 1818 return False 1819 else: 1820 if summary != self.problem['summary'].strip(): 1821 return False 1822 1823 # codes 1824 new_codes = self._PRW_episode_codes.GetData() 1825 if self.problem is None: 1826 if len(new_codes) > 0: 1827 return False 1828 elif self.problem['type'] == u'issue': 1829 if len(new_codes) > 0: 1830 return False 1831 else: 1832 old_code_pks = self.problem.generic_codes 1833 if len(old_code_pks) != len(new_codes): 1834 return False 1835 for code in new_codes: 1836 if code['data'] not in old_code_pks: 1837 return False 1838 1839 return True
1840 1841 empty = property(_get_empty, lambda x:x)
1842 #============================================================
1843 -class cSoapLineTextCtrl(wx_expando.ExpandoTextCtrl):
1844
1845 - def __init__(self, *args, **kwargs):
1846 1847 wx_expando.ExpandoTextCtrl.__init__(self, *args, **kwargs) 1848 1849 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+") 1850 1851 self.__register_interests()
1852 #------------------------------------------------ 1853 # fixup errors in platform expando.py 1854 #------------------------------------------------
1855 - def _wrapLine(self, line, dc, width):
1856 1857 if (wx.MAJOR_VERSION >= 2) and (wx.MINOR_VERSION > 8): 1858 return super(cSoapLineTextCtrl, self)._wrapLine(line, dc, width) 1859 1860 # THIS FIX LIFTED FROM TRUNK IN SVN: 1861 # Estimate where the control will wrap the lines and 1862 # return the count of extra lines needed. 1863 pte = dc.GetPartialTextExtents(line) 1864 width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X) 1865 idx = 0 1866 start = 0 1867 count = 0 1868 spc = -1 1869 while idx < len(pte): 1870 if line[idx] == ' ': 1871 spc = idx 1872 if pte[idx] - start > width: 1873 # we've reached the max width, add a new line 1874 count += 1 1875 # did we see a space? if so restart the count at that pos 1876 if spc != -1: 1877 idx = spc + 1 1878 spc = -1 1879 if idx < len(pte): 1880 start = pte[idx] 1881 else: 1882 idx += 1 1883 return count
1884 #------------------------------------------------ 1885 # event handling 1886 #------------------------------------------------
1887 - def __register_interests(self):
1888 #wx.EVT_KEY_DOWN (self, self.__on_key_down) 1889 #wx.EVT_KEY_UP (self, self.__OnKeyUp) 1890 wx.EVT_CHAR(self, self.__on_char) 1891 wx.EVT_SET_FOCUS(self, self.__on_focus)
1892 #--------------------------------------------------------
1893 - def __on_focus(self, evt):
1894 evt.Skip() 1895 wx.CallAfter(self._after_on_focus)
1896 #--------------------------------------------------------
1897 - def _after_on_focus(self):
1898 #wx.CallAfter(self._adjustCtrl) 1899 evt = wx.PyCommandEvent(wx_expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId()) 1900 evt.SetEventObject(self) 1901 #evt.height = None 1902 #evt.numLines = None 1903 #evt.height = self.GetSize().height 1904 #evt.numLines = self.GetNumberOfLines() 1905 self.GetEventHandler().ProcessEvent(evt)
1906 #--------------------------------------------------------
1907 - def __on_char(self, evt):
1908 char = unichr(evt.GetUnicodeKey()) 1909 1910 if self.LastPosition == 1: 1911 evt.Skip() 1912 return 1913 1914 explicit_expansion = False 1915 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT): # portable CTRL-ALT-... 1916 if evt.GetKeyCode() != 13: 1917 evt.Skip() 1918 return 1919 explicit_expansion = True 1920 1921 if not explicit_expansion: 1922 if self.__keyword_separators.match(char) is None: 1923 evt.Skip() 1924 return 1925 1926 caret_pos, line_no = self.PositionToXY(self.InsertionPoint) 1927 line = self.GetLineText(line_no) 1928 keyword = self.__keyword_separators.split(line[:caret_pos])[-1] 1929 1930 if ( 1931 (not explicit_expansion) 1932 and 1933 (keyword != u'$$steffi') # Easter Egg ;-) 1934 and 1935 (keyword not in [ r[0] for r in gmKeywordExpansion.get_textual_expansion_keywords() ]) 1936 ): 1937 evt.Skip() 1938 return 1939 1940 start = self.InsertionPoint - len(keyword) 1941 wx.CallAfter(self.replace_keyword_with_expansion, keyword, start, explicit_expansion) 1942 1943 evt.Skip() 1944 return
1945 #------------------------------------------------
1946 - def replace_keyword_with_expansion(self, keyword=None, position=None, show_list=False):
1947 1948 expansion = gmKeywordExpansionWidgets.expand_keyword(parent = self, keyword = keyword, show_list = show_list) 1949 1950 if expansion is None: 1951 return 1952 1953 if expansion == u'': 1954 return 1955 1956 self.Replace ( 1957 position, 1958 position + len(keyword), 1959 expansion 1960 ) 1961 1962 self.SetInsertionPoint(position + len(expansion) + 1) 1963 self.ShowPosition(position + len(expansion) + 1) 1964 1965 return
1966 #============================================================ 1967 # visual progress notes 1968 #============================================================
1969 -def configure_visual_progress_note_editor():
1970 1971 def is_valid(value): 1972 1973 if value is None: 1974 gmDispatcher.send ( 1975 signal = 'statustext', 1976 msg = _('You need to actually set an editor.'), 1977 beep = True 1978 ) 1979 return False, value 1980 1981 if value.strip() == u'': 1982 gmDispatcher.send ( 1983 signal = 'statustext', 1984 msg = _('You need to actually set an editor.'), 1985 beep = True 1986 ) 1987 return False, value 1988 1989 found, binary = gmShellAPI.detect_external_binary(value) 1990 if not found: 1991 gmDispatcher.send ( 1992 signal = 'statustext', 1993 msg = _('The command [%s] is not found.') % value, 1994 beep = True 1995 ) 1996 return True, value 1997 1998 return True, binary
1999 #------------------------------------------ 2000 cmd = gmCfgWidgets.configure_string_option ( 2001 message = _( 2002 'Enter the shell command with which to start\n' 2003 'the image editor for visual progress notes.\n' 2004 '\n' 2005 'Any "%(img)s" included with the arguments\n' 2006 'will be replaced by the file name of the\n' 2007 'note template.' 2008 ), 2009 option = u'external.tools.visual_soap_editor_cmd', 2010 bias = 'user', 2011 default_value = None, 2012 validator = is_valid 2013 ) 2014 2015 return cmd 2016 #============================================================
2017 -def select_file_as_visual_progress_note_template(parent=None):
2018 if parent is None: 2019 parent = wx.GetApp().GetTopWindow() 2020 2021 dlg = wx.FileDialog ( 2022 parent = parent, 2023 message = _('Choose file to use as template for new visual progress note'), 2024 defaultDir = os.path.expanduser('~'), 2025 defaultFile = '', 2026 #wildcard = "%s (*)|*|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')), 2027 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST 2028 ) 2029 result = dlg.ShowModal() 2030 2031 if result == wx.ID_CANCEL: 2032 dlg.Destroy() 2033 return None 2034 2035 full_filename = dlg.GetPath() 2036 dlg.Hide() 2037 dlg.Destroy() 2038 return full_filename
2039 #------------------------------------------------------------
2040 -def select_visual_progress_note_template(parent=None):
2041 2042 if parent is None: 2043 parent = wx.GetApp().GetTopWindow() 2044 2045 dlg = gmGuiHelpers.c3ButtonQuestionDlg ( 2046 parent, 2047 -1, 2048 caption = _('Visual progress note source'), 2049 question = _('From which source do you want to pick the image template ?'), 2050 button_defs = [ 2051 {'label': _('Database'), 'tooltip': _('List of templates in the database.'), 'default': True}, 2052 {'label': _('File'), 'tooltip': _('Files in the filesystem.'), 'default': False}, 2053 {'label': _('Device'), 'tooltip': _('Image capture devices (scanners, cameras, etc)'), 'default': False} 2054 ] 2055 ) 2056 result = dlg.ShowModal() 2057 dlg.Destroy() 2058 2059 # 1) select from template 2060 if result == wx.ID_YES: 2061 _log.debug('visual progress note template from: database template') 2062 from Gnumed.wxpython import gmFormWidgets 2063 template = gmFormWidgets.manage_form_templates ( 2064 parent = parent, 2065 template_types = [gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE], 2066 active_only = True 2067 ) 2068 if template is None: 2069 return (None, None) 2070 filename = template.export_to_file() 2071 if filename is None: 2072 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note template for [%s].') % template['name_long']) 2073 return (None, None) 2074 return (filename, True) 2075 2076 # 2) select from disk file 2077 if result == wx.ID_NO: 2078 _log.debug('visual progress note template from: disk file') 2079 fname = select_file_as_visual_progress_note_template(parent = parent) 2080 if fname is None: 2081 return (None, None) 2082 # create a copy of the picked file -- don't modify the original 2083 ext = os.path.splitext(fname)[1] 2084 tmp_name = gmTools.get_unique_filename(suffix = ext) 2085 _log.debug('visual progress note from file: [%s] -> [%s]', fname, tmp_name) 2086 shutil.copy2(fname, tmp_name) 2087 return (tmp_name, False) 2088 2089 # 3) acquire from capture device 2090 if result == wx.ID_CANCEL: 2091 _log.debug('visual progress note template from: image capture device') 2092 fnames = gmDocumentWidgets.acquire_images_from_capture_device(device = None, calling_window = parent) 2093 if fnames is None: 2094 return (None, None) 2095 if len(fnames) == 0: 2096 return (None, None) 2097 return (fnames[0], False) 2098 2099 _log.debug('no visual progress note template source selected') 2100 return (None, None)
2101 #------------------------------------------------------------
2102 -def edit_visual_progress_note(filename=None, episode=None, discard_unmodified=False, doc_part=None, health_issue=None):
2103 """This assumes <filename> contains an image which can be handled by the configured image editor.""" 2104 2105 if doc_part is not None: 2106 filename = doc_part.export_to_file() 2107 if filename is None: 2108 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.')) 2109 return None 2110 2111 dbcfg = gmCfg.cCfgSQL() 2112 cmd = dbcfg.get2 ( 2113 option = u'external.tools.visual_soap_editor_cmd', 2114 workplace = gmSurgery.gmCurrentPractice().active_workplace, 2115 bias = 'user' 2116 ) 2117 2118 if cmd is None: 2119 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = False) 2120 cmd = configure_visual_progress_note_editor() 2121 if cmd is None: 2122 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = True) 2123 return None 2124 2125 if u'%(img)s' in cmd: 2126 cmd = cmd % {u'img': filename} 2127 else: 2128 cmd = u'%s %s' % (cmd, filename) 2129 2130 if discard_unmodified: 2131 original_stat = os.stat(filename) 2132 original_md5 = gmTools.file2md5(filename) 2133 2134 success = gmShellAPI.run_command_in_shell(cmd, blocking = True) 2135 if not success: 2136 gmGuiHelpers.gm_show_error ( 2137 _( 2138 'There was a problem with running the editor\n' 2139 'for visual progress notes.\n' 2140 '\n' 2141 ' [%s]\n' 2142 '\n' 2143 ) % cmd, 2144 _('Editing visual progress note') 2145 ) 2146 return None 2147 2148 try: 2149 open(filename, 'r').close() 2150 except StandardError: 2151 _log.exception('problem accessing visual progress note file [%s]', filename) 2152 gmGuiHelpers.gm_show_error ( 2153 _( 2154 'There was a problem reading the visual\n' 2155 'progress note from the file:\n' 2156 '\n' 2157 ' [%s]\n' 2158 '\n' 2159 ) % filename, 2160 _('Saving visual progress note') 2161 ) 2162 return None 2163 2164 if discard_unmodified: 2165 modified_stat = os.stat(filename) 2166 # same size ? 2167 if original_stat.st_size == modified_stat.st_size: 2168 modified_md5 = gmTools.file2md5(filename) 2169 # same hash ? 2170 if original_md5 == modified_md5: 2171 _log.debug('visual progress note (template) not modified') 2172 # ask user to decide 2173 msg = _( 2174 u'You either created a visual progress note from a template\n' 2175 u'in the database (rather than from a file on disk) or you\n' 2176 u'edited an existing visual progress note.\n' 2177 u'\n' 2178 u'The template/original was not modified at all, however.\n' 2179 u'\n' 2180 u'Do you still want to save the unmodified image as a\n' 2181 u'visual progress note into the EMR of the patient ?\n' 2182 ) 2183 save_unmodified = gmGuiHelpers.gm_show_question ( 2184 msg, 2185 _('Saving visual progress note') 2186 ) 2187 if not save_unmodified: 2188 _log.debug('user discarded unmodified note') 2189 return 2190 2191 if doc_part is not None: 2192 _log.debug('updating visual progress note') 2193 doc_part.update_data_from_file(fname = filename) 2194 doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True) 2195 return None 2196 2197 if not isinstance(episode, gmEMRStructItems.cEpisode): 2198 if episode is None: 2199 episode = _('visual progress notes') 2200 pat = gmPerson.gmCurrentPatient() 2201 emr = pat.get_emr() 2202 episode = emr.add_episode(episode_name = episode.strip(), pk_health_issue = health_issue, is_open = False) 2203 2204 doc = gmDocumentWidgets.save_file_as_new_document ( 2205 filename = filename, 2206 document_type = gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE, 2207 episode = episode, 2208 unlock_patient = False 2209 ) 2210 doc.set_reviewed(technically_abnormal = False, clinically_relevant = True) 2211 2212 return doc
2213 #============================================================
2214 -class cVisualSoapTemplatePhraseWheel(gmPhraseWheel.cPhraseWheel):
2215 """Phrasewheel to allow selection of visual SOAP template.""" 2216
2217 - def __init__(self, *args, **kwargs):
2218 2219 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs) 2220 2221 query = u""" 2222 SELECT 2223 pk AS data, 2224 name_short AS list_label, 2225 name_sort AS field_label 2226 FROM 2227 ref.paperwork_templates 2228 WHERE 2229 fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND ( 2230 name_long %%(fragment_condition)s 2231 OR 2232 name_short %%(fragment_condition)s 2233 ) 2234 ORDER BY list_label 2235 LIMIT 15 2236 """ % gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE 2237 2238 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 2239 mp.setThresholds(2, 3, 5) 2240 2241 self.matcher = mp 2242 self.selection_only = True
2243 #--------------------------------------------------------
2244 - def _data2instance(self):
2245 if self.GetData() is None: 2246 return None 2247 2248 return gmForms.cFormTemplate(aPK_obj = self.GetData())
2249 #============================================================ 2250 from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl 2251
2252 -class cVisualSoapPresenterPnl(wxgVisualSoapPresenterPnl.wxgVisualSoapPresenterPnl):
2253
2254 - def __init__(self, *args, **kwargs):
2255 wxgVisualSoapPresenterPnl.wxgVisualSoapPresenterPnl.__init__(self, *args, **kwargs) 2256 self._SZR_soap = self.GetSizer() 2257 self.__bitmaps = []
2258 #-------------------------------------------------------- 2259 # external API 2260 #--------------------------------------------------------
2261 - def refresh(self, document_folder=None, episodes=None, encounter=None):
2262 2263 self.clear() 2264 if document_folder is not None: 2265 soap_docs = document_folder.get_visual_progress_notes(episodes = episodes, encounter = encounter) 2266 if len(soap_docs) > 0: 2267 for soap_doc in soap_docs: 2268 parts = soap_doc.parts 2269 if len(parts) == 0: 2270 continue 2271 part = parts[0] 2272 fname = part.export_to_file() 2273 if fname is None: 2274 continue 2275 2276 # create bitmap 2277 img = gmGuiHelpers.file2scaled_image ( 2278 filename = fname, 2279 height = 30 2280 ) 2281 #bmp = wx.StaticBitmap(self, -1, img, style = wx.NO_BORDER) 2282 bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER) 2283 2284 # create tooltip 2285 img = gmGuiHelpers.file2scaled_image ( 2286 filename = fname, 2287 height = 150 2288 ) 2289 tip = agw_stt.SuperToolTip ( 2290 u'', 2291 bodyImage = img, 2292 header = _('Created: %s') % part['date_generated'].strftime('%Y %B %d').decode(gmI18N.get_encoding()), 2293 footer = gmTools.coalesce(part['doc_comment'], u'').strip() 2294 ) 2295 tip.SetTopGradientColor('white') 2296 tip.SetMiddleGradientColor('white') 2297 tip.SetBottomGradientColor('white') 2298 tip.SetTarget(bmp) 2299 2300 bmp.doc_part = part 2301 bmp.Bind(wx.EVT_LEFT_UP, self._on_bitmap_leftclicked) 2302 # FIXME: add context menu for Delete/Clone/Add/Configure 2303 self._SZR_soap.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM | wx.EXPAND, 3) 2304 self.__bitmaps.append(bmp) 2305 2306 self.GetParent().Layout()
2307 #--------------------------------------------------------
2308 - def clear(self):
2309 while len(self._SZR_soap.GetChildren()) > 0: 2310 self._SZR_soap.Detach(0) 2311 # for child_idx in range(len(self._SZR_soap.GetChildren())): 2312 # self._SZR_soap.Detach(child_idx) 2313 for bmp in self.__bitmaps: 2314 bmp.Destroy() 2315 self.__bitmaps = []
2316 #--------------------------------------------------------
2317 - def _on_bitmap_leftclicked(self, evt):
2318 wx.CallAfter ( 2319 edit_visual_progress_note, 2320 doc_part = evt.GetEventObject().doc_part, 2321 discard_unmodified = True 2322 )
2323 #============================================================ 2324 from Gnumed.wxGladeWidgets import wxgSimpleSoapPluginPnl 2325
2326 -class cSimpleSoapPluginPnl(wxgSimpleSoapPluginPnl.wxgSimpleSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
2327 - def __init__(self, *args, **kwargs):
2328 2329 wxgSimpleSoapPluginPnl.wxgSimpleSoapPluginPnl.__init__(self, *args, **kwargs) 2330 gmRegetMixin.cRegetOnPaintMixin.__init__(self) 2331 2332 self.__curr_pat = gmPerson.gmCurrentPatient() 2333 self.__problem = None 2334 self.__init_ui() 2335 self.__register_interests()
2336 #----------------------------------------------------- 2337 # internal API 2338 #-----------------------------------------------------
2339 - def __init_ui(self):
2340 self._LCTRL_problems.set_columns(columns = [_('Problem list')]) 2341 self._LCTRL_problems.activate_callback = self._on_problem_activated 2342 self._LCTRL_problems.item_tooltip_callback = self._on_get_problem_tooltip 2343 2344 self._splitter_main.SetSashGravity(0.5) 2345 splitter_width = self._splitter_main.GetSizeTuple()[0] 2346 self._splitter_main.SetSashPosition(splitter_width / 2, True) 2347 2348 self._TCTRL_soap.Disable() 2349 self._BTN_save_soap.Disable() 2350 self._BTN_clear_soap.Disable()
2351 #-----------------------------------------------------
2352 - def __reset_ui(self):
2353 self._LCTRL_problems.set_string_items() 2354 self._TCTRL_soap_problem.SetValue(_('<above, double-click problem to start entering SOAP note>')) 2355 self._TCTRL_soap.SetValue(u'') 2356 self._CHBOX_filter_by_problem.SetLabel(_('&Filter by problem')) 2357 self._TCTRL_journal.SetValue(u'') 2358 2359 self._TCTRL_soap.Disable() 2360 self._BTN_save_soap.Disable() 2361 self._BTN_clear_soap.Disable()
2362 #-----------------------------------------------------
2363 - def __save_soap(self):
2364 if not self.__curr_pat.connected: 2365 return None 2366 2367 if self.__problem is None: 2368 return None 2369 2370 saved = self.__curr_pat.emr.add_clin_narrative ( 2371 note = self._TCTRL_soap.GetValue().strip(), 2372 soap_cat = u'u', 2373 episode = self.__problem 2374 ) 2375 2376 if saved is None: 2377 return False 2378 2379 self._TCTRL_soap.SetValue(u'') 2380 self.__refresh_journal() 2381 return True
2382 #-----------------------------------------------------
2383 - def __perhaps_save_soap(self):
2384 if self._TCTRL_soap.GetValue().strip() == u'': 2385 return True 2386 if self.__problem is None: 2387 # FIXME: this could potentially lose input 2388 self._TCTRL_soap.SetValue(u'') 2389 return None 2390 save_it = gmGuiHelpers.gm_show_question ( 2391 title = _('Saving SOAP note'), 2392 question = _('Do you want to save the SOAP note ?') 2393 ) 2394 if save_it: 2395 return self.__save_soap() 2396 return False
2397 #-----------------------------------------------------
2398 - def __refresh_problem_list(self):
2399 self._LCTRL_problems.set_string_items() 2400 emr = self.__curr_pat.get_emr() 2401 epis = emr.get_episodes(open_status = True) 2402 if len(epis) > 0: 2403 self._LCTRL_problems.set_string_items(items = [ u'%s%s' % ( 2404 e['description'], 2405 gmTools.coalesce(e['health_issue'], u'', u' (%s)') 2406 ) for e in epis ]) 2407 self._LCTRL_problems.set_data(epis)
2408 #-----------------------------------------------------
2409 - def __refresh_journal(self):
2410 self._TCTRL_journal.SetValue(u'') 2411 epi = self._LCTRL_problems.get_selected_item_data(only_one = True) 2412 2413 if epi is not None: 2414 self._CHBOX_filter_by_problem.SetLabel(_('&Filter by problem %s%s%s') % ( 2415 gmTools.u_left_double_angle_quote, 2416 epi['description'], 2417 gmTools.u_right_double_angle_quote 2418 )) 2419 self._CHBOX_filter_by_problem.Refresh() 2420 2421 if not self._CHBOX_filter_by_problem.IsChecked(): 2422 self._TCTRL_journal.SetValue(self.__curr_pat.emr.format_summary(dob = self.__curr_pat['dob'])) 2423 return 2424 2425 if epi is None: 2426 return 2427 2428 self._TCTRL_journal.SetValue(epi.format_as_journal())
2429 #----------------------------------------------------- 2430 # event handling 2431 #-----------------------------------------------------
2432 - def __register_interests(self):
2433 """Configure enabled event signals.""" 2434 # client internal signals 2435 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection) 2436 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection) 2437 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db) 2438 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db) 2439 2440 # synchronous signals 2441 self.__curr_pat.register_pre_selection_callback(callback = self._pre_selection_callback) 2442 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
2443 #-----------------------------------------------------
2444 - def _pre_selection_callback(self):
2445 """Another patient is about to be activated. 2446 2447 Patient change will not proceed before this returns True. 2448 """ 2449 if not self.__curr_pat.connected: 2450 return True 2451 self.__perhaps_save_soap() 2452 self.__problem = None 2453 return True
2454 #-----------------------------------------------------
2455 - def _pre_exit_callback(self):
2456 """The client is about to be shut down. 2457 2458 Shutdown will not proceed before this returns. 2459 """ 2460 if not self.__curr_pat.connected: 2461 return 2462 if not self.__save_soap(): 2463 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save SimpleNotes SOAP note.'), beep = True) 2464 return
2465 #-----------------------------------------------------
2466 - def _on_pre_patient_selection(self):
2467 wx.CallAfter(self.__reset_ui)
2468 #-----------------------------------------------------
2469 - def _on_post_patient_selection(self):
2470 wx.CallAfter(self._schedule_data_reget)
2471 #-----------------------------------------------------
2472 - def _on_episode_issue_mod_db(self):
2473 wx.CallAfter(self._schedule_data_reget)
2474 #-----------------------------------------------------
2475 - def _on_problem_activated(self, event):
2476 self.__perhaps_save_soap() 2477 epi = self._LCTRL_problems.get_selected_item_data(only_one = True) 2478 self._TCTRL_soap_problem.SetValue(_('Progress note: %s%s') % ( 2479 epi['description'], 2480 gmTools.coalesce(epi['health_issue'], u'', u' (%s)') 2481 )) 2482 self.__problem = epi 2483 self._TCTRL_soap.SetValue(u'') 2484 2485 self._TCTRL_soap.Enable() 2486 self._BTN_save_soap.Enable() 2487 self._BTN_clear_soap.Enable()
2488 #-----------------------------------------------------
2489 - def _on_get_problem_tooltip(self, episode):
2490 return episode.format ( 2491 patient = self.__curr_pat, 2492 with_summary = False, 2493 with_codes = True, 2494 with_encounters = False, 2495 with_documents = False, 2496 with_hospital_stays = False, 2497 with_procedures = False, 2498 with_family_history = False, 2499 with_tests = False, 2500 with_vaccinations = False, 2501 with_health_issue = True 2502 )
2503 #-----------------------------------------------------
2504 - def _on_list_item_selected(self, event):
2505 event.Skip() 2506 self.__refresh_journal()
2507 #-----------------------------------------------------
2508 - def _on_filter_by_problem_checked(self, event):
2509 event.Skip() 2510 self.__refresh_journal()
2511 #-----------------------------------------------------
2512 - def _on_add_problem_button_pressed(self, event):
2513 event.Skip() 2514 epi_name = wx.GetTextFromUser ( 2515 _('Please enter a name for the new problem:'), 2516 caption = _('Adding a problem'), 2517 parent = self 2518 ).strip() 2519 if epi_name == u'': 2520 return 2521 self.__curr_pat.emr.add_episode ( 2522 episode_name = epi_name, 2523 pk_health_issue = None, 2524 is_open = True 2525 )
2526 #-----------------------------------------------------
2527 - def _on_edit_problem_button_pressed(self, event):
2528 event.Skip() 2529 epi = self._LCTRL_problems.get_selected_item_data(only_one = True) 2530 if epi is None: 2531 return 2532 gmEMRStructWidgets.edit_episode(parent = self, episode = epi)
2533 #-----------------------------------------------------
2534 - def _on_delete_problem_button_pressed(self, event):
2535 event.Skip() 2536 epi = self._LCTRL_problems.get_selected_item_data(only_one = True) 2537 if epi is None: 2538 return 2539 if not gmEMRStructItems.delete_episode(episode = epi): 2540 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete problem. There is still clinical data recorded for it.'))
2541 #-----------------------------------------------------
2542 - def _on_save_soap_button_pressed(self, event):
2543 event.Skip() 2544 self.__save_soap()
2545 #-----------------------------------------------------
2546 - def _on_clear_soap_button_pressed(self, event):
2547 event.Skip() 2548 self._TCTRL_soap.SetValue(u'')
2549 #----------------------------------------------------- 2550 # reget-on-paint mixin API 2551 #-----------------------------------------------------
2552 - def _populate_with_data(self):
2553 self.__refresh_problem_list() 2554 self.__refresh_journal() 2555 self._TCTRL_soap.SetValue(u'') 2556 return True
2557 2558 #============================================================ 2559 # main 2560 #------------------------------------------------------------ 2561 if __name__ == '__main__': 2562 2563 if len(sys.argv) < 2: 2564 sys.exit() 2565 2566 if sys.argv[1] != 'test': 2567 sys.exit() 2568 2569 gmI18N.activate_locale() 2570 gmI18N.install_domain(domain = 'gnumed') 2571 2572 #----------------------------------------
2573 - def test_select_narrative_from_episodes():
2574 pat = gmPersonSearch.ask_for_patient() 2575 gmPatSearchWidgets.set_active_patient(patient = pat) 2576 app = wx.PyWidgetTester(size = (200, 200)) 2577 sels = select_narrative_from_episodes() 2578 print "selected:" 2579 for sel in sels: 2580 print sel
2581 #----------------------------------------
2582 - def test_cSoapNoteExpandoEditAreaPnl():
2583 pat = gmPersonSearch.ask_for_patient() 2584 application = wx.PyWidgetTester(size=(800,500)) 2585 soap_input = cSoapNoteExpandoEditAreaPnl(application.frame, -1) 2586 application.frame.Show(True) 2587 application.MainLoop()
2588 #----------------------------------------
2589 - def test_cSoapPluginPnl():
2590 patient = gmPersonSearch.ask_for_patient() 2591 if patient is None: 2592 print "No patient. Exiting gracefully..." 2593 return 2594 gmPatSearchWidgets.set_active_patient(patient=patient) 2595 2596 application = wx.PyWidgetTester(size=(800,500)) 2597 soap_input = cSoapPluginPnl(application.frame, -1) 2598 application.frame.Show(True) 2599 soap_input._schedule_data_reget() 2600 application.MainLoop()
2601 #---------------------------------------- 2602 #test_select_narrative_from_episodes() 2603 test_cSoapNoteExpandoEditAreaPnl() 2604 #test_cSoapPluginPnl() 2605 2606 #============================================================ 2607