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

Source Code for Module Gnumed.wxpython.gmNarrativeWidgets

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