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

Source Code for Module Gnumed.wxpython.gmNarrativeWidgets

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