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