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