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  from Gnumed.wxpython import gmTextExpansionWidgets 
  43   
  44  from Gnumed.exporters import gmPatientExporter 
  45   
  46   
  47  _log = logging.getLogger('gm.ui') 
  48  _log.info(__version__) 
  49  #============================================================ 
  50  # narrative related widgets/functions 
  51  #------------------------------------------------------------ 
52 -def move_progress_notes_to_another_encounter(parent=None, encounters=None, episodes=None, patient=None, move_all=False):
53 54 # sanity checks 55 if patient is None: 56 patient = gmPerson.gmCurrentPatient() 57 58 if not patient.connected: 59 gmDispatcher.send(signal = 'statustext', msg = _('Cannot move progress notes. No active patient.')) 60 return False 61 62 if parent is None: 63 parent = wx.GetApp().GetTopWindow() 64 65 emr = patient.get_emr() 66 67 if encounters is None: 68 encs = emr.get_encounters(episodes = episodes) 69 encounters = gmEMRStructWidgets.select_encounters ( 70 parent = parent, 71 patient = patient, 72 single_selection = False, 73 encounters = encs 74 ) 75 # cancelled 76 if encounters is None: 77 return True 78 # none selected 79 if len(encounters) == 0: 80 return True 81 82 notes = emr.get_clin_narrative ( 83 encounters = encounters, 84 episodes = episodes 85 ) 86 87 # which narrative 88 if move_all: 89 selected_narr = notes 90 else: 91 selected_narr = gmListWidgets.get_choices_from_list ( 92 parent = parent, 93 caption = _('Moving progress notes between encounters ...'), 94 single_selection = False, 95 can_return_empty = True, 96 data = notes, 97 msg = _('\n Select the progress notes to move from the list !\n\n'), 98 columns = [_('when'), _('who'), _('type'), _('entry')], 99 choices = [ 100 [ narr['date'].strftime('%x %H:%M'), 101 narr['provider'], 102 gmClinNarrative.soap_cat2l10n[narr['soap_cat']], 103 narr['narrative'].replace('\n', '/').replace('\r', '/') 104 ] for narr in notes 105 ] 106 ) 107 108 if not selected_narr: 109 return True 110 111 # which encounter to move to 112 enc2move2 = gmEMRStructWidgets.select_encounters ( 113 parent = parent, 114 patient = patient, 115 single_selection = True 116 ) 117 118 if not enc2move2: 119 return True 120 121 for narr in selected_narr: 122 narr['pk_encounter'] = enc2move2['pk_encounter'] 123 narr.save() 124 125 return True
126 #------------------------------------------------------------
127 -def manage_progress_notes(parent=None, encounters=None, episodes=None, patient=None):
128 129 # sanity checks 130 if patient is None: 131 patient = gmPerson.gmCurrentPatient() 132 133 if not patient.connected: 134 gmDispatcher.send(signal = 'statustext', msg = _('Cannot edit progress notes. No active patient.')) 135 return False 136 137 if parent is None: 138 parent = wx.GetApp().GetTopWindow() 139 140 emr = patient.get_emr() 141 #-------------------------- 142 def delete(item): 143 if item is None: 144 return False 145 dlg = gmGuiHelpers.c2ButtonQuestionDlg ( 146 parent, 147 -1, 148 caption = _('Deleting progress note'), 149 question = _( 150 'Are you positively sure you want to delete this\n' 151 'progress note from the medical record ?\n' 152 '\n' 153 'Note that even if you chose to delete the entry it will\n' 154 'still be (invisibly) kept in the audit trail to protect\n' 155 'you from litigation because physical deletion is known\n' 156 'to be unlawful in some jurisdictions.\n' 157 ), 158 button_defs = ( 159 {'label': _('Delete'), 'tooltip': _('Yes, delete the progress note.'), 'default': False}, 160 {'label': _('Cancel'), 'tooltip': _('No, do NOT delete the progress note.'), 'default': True} 161 ) 162 ) 163 decision = dlg.ShowModal() 164 165 if decision != wx.ID_YES: 166 return False 167 168 gmClinNarrative.delete_clin_narrative(narrative = item['pk_narrative']) 169 return True
170 #-------------------------- 171 def edit(item): 172 if item is None: 173 return False 174 175 dlg = gmGuiHelpers.cMultilineTextEntryDlg ( 176 parent, 177 -1, 178 title = _('Editing progress note'), 179 msg = _('This is the original progress note:'), 180 data = item.format(left_margin = u' ', fancy = True), 181 text = item['narrative'] 182 ) 183 decision = dlg.ShowModal() 184 185 if decision != wx.ID_SAVE: 186 return False 187 188 val = dlg.value 189 dlg.Destroy() 190 if val.strip() == u'': 191 return False 192 193 item['narrative'] = val 194 item.save_payload() 195 196 return True 197 #-------------------------- 198 def refresh(lctrl): 199 notes = emr.get_clin_narrative ( 200 encounters = encounters, 201 episodes = episodes, 202 providers = [ gmStaff.gmCurrentProvider()['short_alias'] ] 203 ) 204 lctrl.set_string_items(items = [ 205 [ narr['date'].strftime('%x %H:%M'), 206 gmClinNarrative.soap_cat2l10n[narr['soap_cat']], 207 narr['narrative'].replace('\n', '/').replace('\r', '/') 208 ] for narr in notes 209 ]) 210 lctrl.set_data(data = notes) 211 #-------------------------- 212 213 gmListWidgets.get_choices_from_list ( 214 parent = parent, 215 caption = _('Managing progress notes'), 216 msg = _( 217 '\n' 218 ' This list shows the progress notes by %s.\n' 219 '\n' 220 ) % gmStaff.gmCurrentProvider()['short_alias'], 221 columns = [_('when'), _('type'), _('entry')], 222 single_selection = True, 223 can_return_empty = False, 224 edit_callback = edit, 225 delete_callback = delete, 226 refresh_callback = refresh, 227 ignore_OK_button = True 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 #--------------------------------------------------------
1732 - def _on_expando_needs_layout(self, evt):
1733 # need to tell ourselves to re-Layout to refresh scroll bars 1734 1735 # provoke adding scrollbar if needed 1736 #self.Fit() # works on Linux but not on Windows 1737 self.FitInside() # needed on Windows rather than self.Fit() 1738 1739 if self.HasScrollbar(wx.VERTICAL): 1740 # scroll panel to show cursor 1741 expando = self.FindWindowById(evt.GetId()) 1742 y_expando = expando.GetPositionTuple()[1] 1743 h_expando = expando.GetSizeTuple()[1] 1744 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1 1745 if expando.NumberOfLines == 0: 1746 no_of_lines = 1 1747 else: 1748 no_of_lines = expando.NumberOfLines 1749 y_cursor = int(round((float(line_cursor) / no_of_lines) * h_expando)) 1750 y_desired_visible = y_expando + y_cursor 1751 1752 y_view = self.ViewStart[1] 1753 h_view = self.GetClientSizeTuple()[1] 1754 1755 # print "expando:", y_expando, "->", h_expando, ", lines:", expando.NumberOfLines 1756 # print "cursor :", y_cursor, "at line", line_cursor, ", insertion point:", expando.GetInsertionPoint() 1757 # print "wanted :", y_desired_visible 1758 # print "view-y :", y_view 1759 # print "scroll2:", h_view 1760 1761 # expando starts before view 1762 if y_desired_visible < y_view: 1763 # print "need to scroll up" 1764 self.Scroll(0, y_desired_visible) 1765 1766 if y_desired_visible > h_view: 1767 # print "need to scroll down" 1768 self.Scroll(0, y_desired_visible)
1769 #-------------------------------------------------------- 1770 # properties 1771 #--------------------------------------------------------
1772 - def _get_soap(self):
1773 soap_notes = [] 1774 1775 tmp = self._TCTRL_Soap.GetValue().strip() 1776 if tmp != u'': 1777 soap_notes.append(['s', tmp]) 1778 1779 tmp = self._TCTRL_sOap.GetValue().strip() 1780 if tmp != u'': 1781 soap_notes.append(['o', tmp]) 1782 1783 tmp = self._TCTRL_soAp.GetValue().strip() 1784 if tmp != u'': 1785 soap_notes.append(['a', tmp]) 1786 1787 tmp = self._TCTRL_soaP.GetValue().strip() 1788 if tmp != u'': 1789 soap_notes.append(['p', tmp]) 1790 1791 return soap_notes
1792 1793 soap = property(_get_soap, lambda x:x) 1794 #--------------------------------------------------------
1795 - def _get_empty(self):
1796 1797 # soap fields 1798 for field in self.soap_fields: 1799 if field.GetValue().strip() != u'': 1800 return False 1801 1802 # summary 1803 summary = self._TCTRL_episode_summary.GetValue().strip() 1804 if self.problem is None: 1805 if summary != u'': 1806 return False 1807 elif self.problem['type'] == u'issue': 1808 if summary != u'': 1809 return False 1810 else: 1811 if self.problem['summary'] is None: 1812 if summary != u'': 1813 return False 1814 else: 1815 if summary != self.problem['summary'].strip(): 1816 return False 1817 1818 # codes 1819 new_codes = self._PRW_episode_codes.GetData() 1820 if self.problem is None: 1821 if len(new_codes) > 0: 1822 return False 1823 elif self.problem['type'] == u'issue': 1824 if len(new_codes) > 0: 1825 return False 1826 else: 1827 old_code_pks = self.problem.generic_codes 1828 if len(old_code_pks) != len(new_codes): 1829 return False 1830 for code in new_codes: 1831 if code['data'] not in old_code_pks: 1832 return False 1833 1834 return True
1835 1836 empty = property(_get_empty, lambda x:x)
1837 #============================================================
1838 -class cSoapLineTextCtrl(wx_expando.ExpandoTextCtrl):
1839
1840 - def __init__(self, *args, **kwargs):
1841 1842 wx_expando.ExpandoTextCtrl.__init__(self, *args, **kwargs) 1843 1844 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+") 1845 1846 self.__register_interests()
1847 #------------------------------------------------ 1848 # fixup errors in platform expando.py 1849 #------------------------------------------------
1850 - def _wrapLine(self, line, dc, width):
1851 1852 if (wx.MAJOR_VERSION >= 2) and (wx.MINOR_VERSION > 8): 1853 return super(cSoapLineTextCtrl, self)._wrapLine(line, dc, width) 1854 1855 # THIS FIX LIFTED FROM TRUNK IN SVN: 1856 # Estimate where the control will wrap the lines and 1857 # return the count of extra lines needed. 1858 pte = dc.GetPartialTextExtents(line) 1859 width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X) 1860 idx = 0 1861 start = 0 1862 count = 0 1863 spc = -1 1864 while idx < len(pte): 1865 if line[idx] == ' ': 1866 spc = idx 1867 if pte[idx] - start > width: 1868 # we've reached the max width, add a new line 1869 count += 1 1870 # did we see a space? if so restart the count at that pos 1871 if spc != -1: 1872 idx = spc + 1 1873 spc = -1 1874 if idx < len(pte): 1875 start = pte[idx] 1876 else: 1877 idx += 1 1878 return count
1879 #------------------------------------------------ 1880 # event handling 1881 #------------------------------------------------
1882 - def __register_interests(self):
1883 #wx.EVT_KEY_DOWN (self, self.__on_key_down) 1884 #wx.EVT_KEY_UP (self, self.__OnKeyUp) 1885 wx.EVT_CHAR(self, self.__on_char) 1886 wx.EVT_SET_FOCUS(self, self.__on_focus)
1887 #--------------------------------------------------------
1888 - def __on_focus(self, evt):
1889 evt.Skip() 1890 wx.CallAfter(self._after_on_focus)
1891 #--------------------------------------------------------
1892 - def _after_on_focus(self):
1893 #wx.CallAfter(self._adjustCtrl) 1894 evt = wx.PyCommandEvent(wx_expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId()) 1895 evt.SetEventObject(self) 1896 #evt.height = None 1897 #evt.numLines = None 1898 #evt.height = self.GetSize().height 1899 #evt.numLines = self.GetNumberOfLines() 1900 self.GetEventHandler().ProcessEvent(evt)
1901 #--------------------------------------------------------
1902 - def __on_char(self, evt):
1903 char = unichr(evt.GetUnicodeKey()) 1904 1905 if self.LastPosition == 1: 1906 evt.Skip() 1907 return 1908 1909 explicit_expansion = False 1910 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT): # portable CTRL-ALT-... 1911 if evt.GetKeyCode() != 13: 1912 evt.Skip() 1913 return 1914 explicit_expansion = True 1915 1916 if not explicit_expansion: 1917 if self.__keyword_separators.match(char) is None: 1918 evt.Skip() 1919 return 1920 1921 caret_pos, line_no = self.PositionToXY(self.InsertionPoint) 1922 line = self.GetLineText(line_no) 1923 keyword = self.__keyword_separators.split(line[:caret_pos])[-1] 1924 1925 if ( 1926 (not explicit_expansion) 1927 and 1928 (keyword != u'$$steffi') # Easter Egg ;-) 1929 and 1930 (keyword not in [ r[0] for r in gmPG2.get_text_expansion_keywords() ]) 1931 ): 1932 evt.Skip() 1933 return 1934 1935 start = self.InsertionPoint - len(keyword) 1936 wx.CallAfter(self.replace_keyword_with_expansion, keyword, start, explicit_expansion) 1937 1938 evt.Skip() 1939 return
1940 #------------------------------------------------
1941 - def replace_keyword_with_expansion(self, keyword=None, position=None, show_list=False):
1942 1943 expansion = gmTextExpansionWidgets.expand_keyword(parent = self, keyword = keyword, show_list = show_list) 1944 1945 if expansion is None: 1946 return 1947 1948 if expansion == u'': 1949 return 1950 1951 self.Replace ( 1952 position, 1953 position + len(keyword), 1954 expansion 1955 ) 1956 1957 self.SetInsertionPoint(position + len(expansion) + 1) 1958 self.ShowPosition(position + len(expansion) + 1) 1959 1960 return
1961 #============================================================ 1962 # visual progress notes 1963 #============================================================
1964 -def configure_visual_progress_note_editor():
1965 1966 def is_valid(value): 1967 1968 if value is None: 1969 gmDispatcher.send ( 1970 signal = 'statustext', 1971 msg = _('You need to actually set an editor.'), 1972 beep = True 1973 ) 1974 return False, value 1975 1976 if value.strip() == u'': 1977 gmDispatcher.send ( 1978 signal = 'statustext', 1979 msg = _('You need to actually set an editor.'), 1980 beep = True 1981 ) 1982 return False, value 1983 1984 found, binary = gmShellAPI.detect_external_binary(value) 1985 if not found: 1986 gmDispatcher.send ( 1987 signal = 'statustext', 1988 msg = _('The command [%s] is not found.') % value, 1989 beep = True 1990 ) 1991 return True, value 1992 1993 return True, binary
1994 #------------------------------------------ 1995 cmd = gmCfgWidgets.configure_string_option ( 1996 message = _( 1997 'Enter the shell command with which to start\n' 1998 'the image editor for visual progress notes.\n' 1999 '\n' 2000 'Any "%(img)s" included with the arguments\n' 2001 'will be replaced by the file name of the\n' 2002 'note template.' 2003 ), 2004 option = u'external.tools.visual_soap_editor_cmd', 2005 bias = 'user', 2006 default_value = None, 2007 validator = is_valid 2008 ) 2009 2010 return cmd 2011 #============================================================
2012 -def select_file_as_visual_progress_note_template(parent=None):
2013 if parent is None: 2014 parent = wx.GetApp().GetTopWindow() 2015 2016 dlg = wx.FileDialog ( 2017 parent = parent, 2018 message = _('Choose file to use as template for new visual progress note'), 2019 defaultDir = os.path.expanduser('~'), 2020 defaultFile = '', 2021 #wildcard = "%s (*)|*|%s (*.*)|*.*" % (_('all files'), _('all files (Win)')), 2022 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST 2023 ) 2024 result = dlg.ShowModal() 2025 2026 if result == wx.ID_CANCEL: 2027 dlg.Destroy() 2028 return None 2029 2030 full_filename = dlg.GetPath() 2031 dlg.Hide() 2032 dlg.Destroy() 2033 return full_filename
2034 #------------------------------------------------------------
2035 -def select_visual_progress_note_template(parent=None):
2036 2037 if parent is None: 2038 parent = wx.GetApp().GetTopWindow() 2039 2040 dlg = gmGuiHelpers.c3ButtonQuestionDlg ( 2041 parent, 2042 -1, 2043 caption = _('Visual progress note source'), 2044 question = _('From which source do you want to pick the image template ?'), 2045 button_defs = [ 2046 {'label': _('Database'), 'tooltip': _('List of templates in the database.'), 'default': True}, 2047 {'label': _('File'), 'tooltip': _('Files in the filesystem.'), 'default': False}, 2048 {'label': _('Device'), 'tooltip': _('Image capture devices (scanners, cameras, etc)'), 'default': False} 2049 ] 2050 ) 2051 result = dlg.ShowModal() 2052 dlg.Destroy() 2053 2054 # 1) select from template 2055 if result == wx.ID_YES: 2056 _log.debug('visual progress note template from: database template') 2057 from Gnumed.wxpython import gmFormWidgets 2058 template = gmFormWidgets.manage_form_templates ( 2059 parent = parent, 2060 template_types = [gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE], 2061 active_only = True 2062 ) 2063 if template is None: 2064 return (None, None) 2065 filename = template.export_to_file() 2066 if filename is None: 2067 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note template for [%s].') % template['name_long']) 2068 return (None, None) 2069 return (filename, True) 2070 2071 # 2) select from disk file 2072 if result == wx.ID_NO: 2073 _log.debug('visual progress note template from: disk file') 2074 fname = select_file_as_visual_progress_note_template(parent = parent) 2075 if fname is None: 2076 return (None, None) 2077 # create a copy of the picked file -- don't modify the original 2078 ext = os.path.splitext(fname)[1] 2079 tmp_name = gmTools.get_unique_filename(suffix = ext) 2080 _log.debug('visual progress note from file: [%s] -> [%s]', fname, tmp_name) 2081 shutil.copy2(fname, tmp_name) 2082 return (tmp_name, False) 2083 2084 # 3) acquire from capture device 2085 if result == wx.ID_CANCEL: 2086 _log.debug('visual progress note template from: image capture device') 2087 fnames = gmDocumentWidgets.acquire_images_from_capture_device(device = None, calling_window = parent) 2088 if fnames is None: 2089 return (None, None) 2090 if len(fnames) == 0: 2091 return (None, None) 2092 return (fnames[0], False) 2093 2094 _log.debug('no visual progress note template source selected') 2095 return (None, None)
2096 #------------------------------------------------------------
2097 -def edit_visual_progress_note(filename=None, episode=None, discard_unmodified=False, doc_part=None, health_issue=None):
2098 """This assumes <filename> contains an image which can be handled by the configured image editor.""" 2099 2100 if doc_part is not None: 2101 filename = doc_part.export_to_file() 2102 if filename is None: 2103 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.')) 2104 return None 2105 2106 dbcfg = gmCfg.cCfgSQL() 2107 cmd = dbcfg.get2 ( 2108 option = u'external.tools.visual_soap_editor_cmd', 2109 workplace = gmSurgery.gmCurrentPractice().active_workplace, 2110 bias = 'user' 2111 ) 2112 2113 if cmd is None: 2114 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = False) 2115 cmd = configure_visual_progress_note_editor() 2116 if cmd is None: 2117 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = True) 2118 return None 2119 2120 if u'%(img)s' in cmd: 2121 cmd % {u'img': filename} 2122 else: 2123 cmd = u'%s %s' % (cmd, filename) 2124 2125 if discard_unmodified: 2126 original_stat = os.stat(filename) 2127 original_md5 = gmTools.file2md5(filename) 2128 2129 success = gmShellAPI.run_command_in_shell(cmd, blocking = True) 2130 if not success: 2131 gmGuiHelpers.gm_show_error ( 2132 _( 2133 'There was a problem with running the editor\n' 2134 'for visual progress notes.\n' 2135 '\n' 2136 ' [%s]\n' 2137 '\n' 2138 ) % cmd, 2139 _('Editing visual progress note') 2140 ) 2141 return None 2142 2143 try: 2144 open(filename, 'r').close() 2145 except StandardError: 2146 _log.exception('problem accessing visual progress note file [%s]', filename) 2147 gmGuiHelpers.gm_show_error ( 2148 _( 2149 'There was a problem reading the visual\n' 2150 'progress note from the file:\n' 2151 '\n' 2152 ' [%s]\n' 2153 '\n' 2154 ) % filename, 2155 _('Saving visual progress note') 2156 ) 2157 return None 2158 2159 if discard_unmodified: 2160 modified_stat = os.stat(filename) 2161 # same size ? 2162 if original_stat.st_size == modified_stat.st_size: 2163 modified_md5 = gmTools.file2md5(filename) 2164 # same hash ? 2165 if original_md5 == modified_md5: 2166 _log.debug('visual progress note (template) not modified') 2167 # ask user to decide 2168 msg = _( 2169 u'You either created a visual progress note from a template\n' 2170 u'in the database (rather than from a file on disk) or you\n' 2171 u'edited an existing visual progress note.\n' 2172 u'\n' 2173 u'The template/original was not modified at all, however.\n' 2174 u'\n' 2175 u'Do you still want to save the unmodified image as a\n' 2176 u'visual progress note into the EMR of the patient ?\n' 2177 ) 2178 save_unmodified = gmGuiHelpers.gm_show_question ( 2179 msg, 2180 _('Saving visual progress note') 2181 ) 2182 if not save_unmodified: 2183 _log.debug('user discarded unmodified note') 2184 return 2185 2186 if doc_part is not None: 2187 doc_part.update_data_from_file(fname = filename) 2188 doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True) 2189 return None 2190 2191 if not isinstance(episode, gmEMRStructItems.cEpisode): 2192 if episode is None: 2193 episode = _('visual progress notes') 2194 pat = gmPerson.gmCurrentPatient() 2195 emr = pat.get_emr() 2196 episode = emr.add_episode(episode_name = episode.strip(), pk_health_issue = health_issue, is_open = False) 2197 2198 doc = gmDocumentWidgets.save_file_as_new_document ( 2199 filename = filename, 2200 document_type = gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE, 2201 episode = episode, 2202 unlock_patient = True 2203 ) 2204 doc.set_reviewed(technically_abnormal = False, clinically_relevant = True) 2205 2206 return doc
2207 #============================================================
2208 -class cVisualSoapTemplatePhraseWheel(gmPhraseWheel.cPhraseWheel):
2209 """Phrasewheel to allow selection of visual SOAP template.""" 2210
2211 - def __init__(self, *args, **kwargs):
2212 2213 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs) 2214 2215 query = u""" 2216 SELECT 2217 pk AS data, 2218 name_short AS list_label, 2219 name_sort AS field_label 2220 FROM 2221 ref.paperwork_templates 2222 WHERE 2223 fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND ( 2224 name_long %%(fragment_condition)s 2225 OR 2226 name_short %%(fragment_condition)s 2227 ) 2228 ORDER BY list_label 2229 LIMIT 15 2230 """ % gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE 2231 2232 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query]) 2233 mp.setThresholds(2, 3, 5) 2234 2235 self.matcher = mp 2236 self.selection_only = True
2237 #--------------------------------------------------------
2238 - def _data2instance(self):
2239 if self.GetData() is None: 2240 return None 2241 2242 return gmForms.cFormTemplate(aPK_obj = self.GetData())
2243 #============================================================ 2244 from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl 2245
2246 -class cVisualSoapPresenterPnl(wxgVisualSoapPresenterPnl.wxgVisualSoapPresenterPnl):
2247
2248 - def __init__(self, *args, **kwargs):
2249 wxgVisualSoapPresenterPnl.wxgVisualSoapPresenterPnl.__init__(self, *args, **kwargs) 2250 self._SZR_soap = self.GetSizer() 2251 self.__bitmaps = []
2252 #-------------------------------------------------------- 2253 # external API 2254 #--------------------------------------------------------
2255 - def refresh(self, document_folder=None, episodes=None, encounter=None):
2256 2257 self.clear() 2258 if document_folder is not None: 2259 soap_docs = document_folder.get_visual_progress_notes(episodes = episodes, encounter = encounter) 2260 if len(soap_docs) > 0: 2261 for soap_doc in soap_docs: 2262 parts = soap_doc.parts 2263 if len(parts) == 0: 2264 continue 2265 part = parts[0] 2266 fname = part.export_to_file() 2267 if fname is None: 2268 continue 2269 2270 # create bitmap 2271 img = gmGuiHelpers.file2scaled_image ( 2272 filename = fname, 2273 height = 30 2274 ) 2275 #bmp = wx.StaticBitmap(self, -1, img, style = wx.NO_BORDER) 2276 bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER) 2277 2278 # create tooltip 2279 img = gmGuiHelpers.file2scaled_image ( 2280 filename = fname, 2281 height = 150 2282 ) 2283 tip = agw_stt.SuperToolTip ( 2284 u'', 2285 bodyImage = img, 2286 header = _('Created: %s') % part['date_generated'].strftime('%Y %B %d').decode(gmI18N.get_encoding()), 2287 footer = gmTools.coalesce(part['doc_comment'], u'').strip() 2288 ) 2289 tip.SetTopGradientColor('white') 2290 tip.SetMiddleGradientColor('white') 2291 tip.SetBottomGradientColor('white') 2292 tip.SetTarget(bmp) 2293 2294 bmp.doc_part = part 2295 bmp.Bind(wx.EVT_LEFT_UP, self._on_bitmap_leftclicked) 2296 # FIXME: add context menu for Delete/Clone/Add/Configure 2297 self._SZR_soap.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM | wx.EXPAND, 3) 2298 self.__bitmaps.append(bmp) 2299 2300 self.GetParent().Layout()
2301 #--------------------------------------------------------
2302 - def clear(self):
2303 while len(self._SZR_soap.GetChildren()) > 0: 2304 self._SZR_soap.Detach(0) 2305 # for child_idx in range(len(self._SZR_soap.GetChildren())): 2306 # self._SZR_soap.Detach(child_idx) 2307 for bmp in self.__bitmaps: 2308 bmp.Destroy() 2309 self.__bitmaps = []
2310 #--------------------------------------------------------
2311 - def _on_bitmap_leftclicked(self, evt):
2312 wx.CallAfter ( 2313 edit_visual_progress_note, 2314 doc_part = evt.GetEventObject().doc_part, 2315 discard_unmodified = True 2316 )
2317 #============================================================ 2318 from Gnumed.wxGladeWidgets import wxgSimpleSoapPluginPnl 2319
2320 -class cSimpleSoapPluginPnl(wxgSimpleSoapPluginPnl.wxgSimpleSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
2321 - def __init__(self, *args, **kwargs):
2322 2323 wxgSimpleSoapPluginPnl.wxgSimpleSoapPluginPnl.__init__(self, *args, **kwargs) 2324 gmRegetMixin.cRegetOnPaintMixin.__init__(self) 2325 2326 self.__pat = gmPerson.gmCurrentPatient() 2327 self.__problem = None 2328 self.__init_ui() 2329 self.__register_interests()
2330 #----------------------------------------------------- 2331 # internal API 2332 #-----------------------------------------------------
2333 - def __init_ui(self):
2334 self._LCTRL_problems.set_columns(columns = [_('Problem list')]) 2335 self._LCTRL_problems.activate_callback = self._on_problem_activated 2336 self._LCTRL_problems.item_tooltip_callback = self._on_get_problem_tooltip 2337 2338 self._splitter_main.SetSashGravity(0.5) 2339 splitter_width = self._splitter_main.GetSizeTuple()[0] 2340 self._splitter_main.SetSashPosition(splitter_width / 2, True)
2341 #-----------------------------------------------------
2342 - def __reset_ui(self):
2343 self._LCTRL_problems.set_string_items() 2344 self._TCTRL_soap_problem.SetValue(u'') 2345 self._TCTRL_soap.SetValue(u'') 2346 self._CHBOX_filter_by_problem.SetLabel(_('&Filter by problem')) 2347 self._TCTRL_journal.SetValue(u'')
2348 #-----------------------------------------------------
2349 - def __save_soap(self):
2350 if not self.__pat.connected: 2351 return None 2352 2353 if self.__problem is None: 2354 return None 2355 2356 saved = self.__pat.emr.add_clin_narrative ( 2357 note = self._TCTRL_soap.GetValue().strip(), 2358 soap_cat = u'u', 2359 episode = self.__problem 2360 ) 2361 2362 if saved is None: 2363 return False 2364 2365 self._TCTRL_soap.SetValue(u'') 2366 self.__refresh_journal() 2367 return True
2368 #-----------------------------------------------------
2369 - def __perhaps_save_soap(self):
2370 if self._TCTRL_soap.GetValue().strip() == u'': 2371 return True 2372 save_it = gmGuiHelpers.gm_show_question ( 2373 title = _('Saving SOAP note'), 2374 question = _('Do you want to save the SOAP note ?') 2375 ) 2376 if save_it: 2377 return self.__save_soap() 2378 return False
2379 #-----------------------------------------------------
2380 - def __refresh_problem_list(self):
2381 self._LCTRL_problems.set_string_items() 2382 emr = self.__pat.get_emr() 2383 epis = emr.get_episodes(open_status = True) 2384 if len(epis) > 0: 2385 self._LCTRL_problems.set_string_items(items = [ u'%s%s' % ( 2386 e['description'], 2387 gmTools.coalesce(e['health_issue'], u'', u' (%s)') 2388 ) for e in epis ]) 2389 self._LCTRL_problems.set_data(epis)
2390 #-----------------------------------------------------
2391 - def __refresh_journal(self):
2392 self._TCTRL_journal.SetValue(u'') 2393 epi = self._LCTRL_problems.get_selected_item_data(only_one = True) 2394 2395 if epi is not None: 2396 self._CHBOX_filter_by_problem.SetLabel(_('&Filter by problem %s%s%s') % ( 2397 gmTools.u_left_double_angle_quote, 2398 epi['description'], 2399 gmTools.u_right_double_angle_quote 2400 )) 2401 self._CHBOX_filter_by_problem.Refresh() 2402 2403 if not self._CHBOX_filter_by_problem.IsChecked(): 2404 self._TCTRL_journal.SetValue(self.__pat.emr.format_summary(dob = self.__pat['dob'])) 2405 return 2406 2407 if epi is None: 2408 return 2409 2410 self._TCTRL_journal.SetValue(epi.format_as_journal())
2411 #----------------------------------------------------- 2412 # event handling 2413 #-----------------------------------------------------
2414 - def __register_interests(self):
2415 """Configure enabled event signals.""" 2416 # client internal signals 2417 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection) 2418 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection) 2419 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db) 2420 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db) 2421 2422 # synchronous signals 2423 self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback) 2424 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
2425 #-----------------------------------------------------
2426 - def _pre_selection_callback(self):
2427 """Another patient is about to be activated. 2428 2429 Patient change will not proceed before this returns True. 2430 """ 2431 if not self.__pat.connected: 2432 return True 2433 self.__perhaps_save_soap() 2434 self.__problem = None 2435 return True
2436 #-----------------------------------------------------
2437 - def _pre_exit_callback(self):
2438 """The client is about to be shut down. 2439 2440 Shutdown will not proceed before this returns. 2441 """ 2442 if not self.__pat.connected: 2443 return 2444 if not self.__save_soap(): 2445 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save SimpleNotes SOAP note.'), beep = True) 2446 return
2447 #-----------------------------------------------------
2448 - def _on_pre_patient_selection(self):
2449 wx.CallAfter(self.__reset_ui)
2450 #-----------------------------------------------------
2451 - def _on_post_patient_selection(self):
2452 wx.CallAfter(self._schedule_data_reget)
2453 #-----------------------------------------------------
2454 - def _on_episode_issue_mod_db(self):
2455 wx.CallAfter(self._schedule_data_reget)
2456 #-----------------------------------------------------
2457 - def _on_problem_activated(self, event):
2458 self.__perhaps_save_soap() 2459 epi = self._LCTRL_problems.get_selected_item_data(only_one = True) 2460 self._TCTRL_soap_problem.SetValue(_('Progress note: %s%s') % ( 2461 epi['description'], 2462 gmTools.coalesce(epi['health_issue'], u'', u' (%s)') 2463 )) 2464 self.__problem = epi 2465 self._TCTRL_soap.SetValue(u'')
2466 #-----------------------------------------------------
2467 - def _on_get_problem_tooltip(self, episode):
2468 return episode.format ( 2469 patient = self.__pat, 2470 with_summary = False, 2471 with_codes = True, 2472 with_encounters = False, 2473 with_documents = False, 2474 with_hospital_stays = False, 2475 with_procedures = False, 2476 with_family_history = False, 2477 with_tests = False, 2478 with_vaccinations = False, 2479 with_health_issue = True 2480 )
2481 #-----------------------------------------------------
2482 - def _on_list_item_selected(self, event):
2483 event.Skip() 2484 self.__refresh_journal()
2485 #-----------------------------------------------------
2486 - def _on_filter_by_problem_checked(self, event):
2487 event.Skip() 2488 self.__refresh_journal()
2489 #-----------------------------------------------------
2490 - def _on_add_problem_button_pressed(self, event):
2491 event.Skip() 2492 epi_name = wx.GetTextFromUser ( 2493 _('Please enter a name for the new problem:'), 2494 caption = _('Adding a problem'), 2495 parent = self 2496 ).strip() 2497 if epi_name == u'': 2498 return 2499 self.__pat.emr.add_episode ( 2500 episode_name = epi_name, 2501 pk_health_issue = None, 2502 is_open = True 2503 )
2504 #-----------------------------------------------------
2505 - def _on_edit_problem_button_pressed(self, event):
2506 event.Skip() 2507 epi = self._LCTRL_problems.get_selected_item_data(only_one = True) 2508 if epi is None: 2509 return 2510 gmEMRStructWidgets.edit_episode(parent = self, episode = epi)
2511 #-----------------------------------------------------
2512 - def _on_delete_problem_button_pressed(self, event):
2513 event.Skip() 2514 epi = self._LCTRL_problems.get_selected_item_data(only_one = True) 2515 if epi is None: 2516 return 2517 if not gmEMRStructItems.delete_episode(episode = epi): 2518 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete problem. There is still clinical data recorded for it.'))
2519 #-----------------------------------------------------
2520 - def _on_save_soap_button_pressed(self, event):
2521 event.Skip() 2522 self.__save_soap()
2523 #-----------------------------------------------------
2524 - def _on_clear_soap_button_pressed(self, event):
2525 event.Skip() 2526 self._TCTRL_soap.SetValue(u'')
2527 #----------------------------------------------------- 2528 # reget-on-paint mixin API 2529 #-----------------------------------------------------
2530 - def _populate_with_data(self):
2531 self.__refresh_problem_list() 2532 self.__refresh_journal() 2533 self._TCTRL_soap.SetValue(u'') 2534 return True
2535 2536 #============================================================ 2537 # main 2538 #------------------------------------------------------------ 2539 if __name__ == '__main__': 2540 2541 if len(sys.argv) < 2: 2542 sys.exit() 2543 2544 if sys.argv[1] != 'test': 2545 sys.exit() 2546 2547 gmI18N.activate_locale() 2548 gmI18N.install_domain(domain = 'gnumed') 2549 2550 #----------------------------------------
2551 - def test_select_narrative_from_episodes():
2552 pat = gmPersonSearch.ask_for_patient() 2553 gmPatSearchWidgets.set_active_patient(patient = pat) 2554 app = wx.PyWidgetTester(size = (200, 200)) 2555 sels = select_narrative_from_episodes() 2556 print "selected:" 2557 for sel in sels: 2558 print sel
2559 #----------------------------------------
2560 - def test_cSoapNoteExpandoEditAreaPnl():
2561 pat = gmPersonSearch.ask_for_patient() 2562 application = wx.PyWidgetTester(size=(800,500)) 2563 soap_input = cSoapNoteExpandoEditAreaPnl(application.frame, -1) 2564 application.frame.Show(True) 2565 application.MainLoop()
2566 #----------------------------------------
2567 - def test_cSoapPluginPnl():
2568 patient = gmPersonSearch.ask_for_patient() 2569 if patient is None: 2570 print "No patient. Exiting gracefully..." 2571 return 2572 gmPatSearchWidgets.set_active_patient(patient=patient) 2573 2574 application = wx.PyWidgetTester(size=(800,500)) 2575 soap_input = cSoapPluginPnl(application.frame, -1) 2576 application.frame.Show(True) 2577 soap_input._schedule_data_reget() 2578 application.MainLoop()
2579 #---------------------------------------- 2580 #test_select_narrative_from_episodes() 2581 test_cSoapNoteExpandoEditAreaPnl() 2582 #test_cSoapPluginPnl() 2583 2584 #============================================================ 2585