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