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