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