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

Source Code for Module Gnumed.wxpython.gmNarrativeWidgets

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