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

Source Code for Module Gnumed.wxpython.gmNarrativeWidgets

   1  """GNUmed narrative handling widgets.""" 
   2  #================================================================ 
   3  # $Source: /cvsroot/gnumed/gnumed/gnumed/client/wxpython/gmNarrativeWidgets.py,v $ 
   4  # $Id: gmNarrativeWidgets.py,v 1.45 2010/01/11 19:51:09 ncq Exp $ 
   5  __version__ = "$Revision: 1.45 $" 
   6  __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>" 
   7   
   8  import sys, logging, os, os.path, time, re as regex 
   9   
  10   
  11  import wx 
  12  import wx.lib.expando as wxexpando 
  13   
  14   
  15  if __name__ == '__main__': 
  16          sys.path.insert(0, '../../') 
  17  from Gnumed.pycommon import gmI18N, gmDispatcher, gmTools, gmDateTime, gmPG2, gmCfg 
  18  from Gnumed.business import gmPerson, gmEMRStructItems, gmClinNarrative, gmSurgery 
  19  from Gnumed.exporters import gmPatientExporter 
  20  from Gnumed.wxpython import gmListWidgets, gmEMRStructWidgets, gmRegetMixin, gmGuiHelpers, gmPatSearchWidgets 
  21  from Gnumed.wxGladeWidgets import wxgMoveNarrativeDlg, wxgSoapNoteExpandoEditAreaPnl 
  22   
  23   
  24  _log = logging.getLogger('gm.ui') 
  25  _log.info(__version__) 
  26  #============================================================ 
  27  # narrative related widgets/functions 
  28  #------------------------------------------------------------ 
29 -def move_progress_notes_to_another_encounter(parent=None, encounters=None, episodes=None, patient=None, move_all=False):
30 31 # sanity checks 32 if patient is None: 33 patient = gmPerson.gmCurrentPatient() 34 35 if not patient.connected: 36 gmDispatcher.send(signal = 'statustext', msg = _('Cannot move progress notes. No active patient.')) 37 return False 38 39 if parent is None: 40 parent = wx.GetApp().GetTopWindow() 41 42 emr = patient.get_emr() 43 44 if encounters is None: 45 encs = emr.get_encounters(episodes = episodes) 46 encounters = gmEMRStructWidgets.select_encounters ( 47 parent = parent, 48 patient = patient, 49 single_selection = False, 50 encounters = encs 51 ) 52 53 notes = emr.get_clin_narrative ( 54 encounters = encounters, 55 episodes = episodes 56 ) 57 58 # which narrative 59 if move_all: 60 selected_narr = notes 61 else: 62 selected_narr = gmListWidgets.get_choices_from_list ( 63 parent = parent, 64 caption = _('Moving progress notes between encounters ...'), 65 single_selection = False, 66 can_return_empty = True, 67 data = notes, 68 msg = _('\n Select the progress notes to move from the list !\n\n'), 69 columns = [_('when'), _('who'), _('type'), _('entry')], 70 choices = [ 71 [ narr['date'].strftime('%x %H:%M'), 72 narr['provider'], 73 gmClinNarrative.soap_cat2l10n[narr['soap_cat']], 74 narr['narrative'].replace('\n', '/').replace('\r', '/') 75 ] for narr in notes 76 ] 77 ) 78 79 if not selected_narr: 80 return True 81 82 # which encounter to move to 83 enc2move2 = gmEMRStructWidgets.select_encounters ( 84 parent = parent, 85 patient = patient, 86 single_selection = True 87 ) 88 89 if not enc2move2: 90 return True 91 92 for narr in selected_narr: 93 narr['pk_encounter'] = enc2move2['pk_encounter'] 94 narr.save() 95 96 return True
97 #------------------------------------------------------------
98 -def manage_progress_notes(parent=None, encounters=None, episodes=None, patient=None):
99 100 # sanity checks 101 if patient is None: 102 patient = gmPerson.gmCurrentPatient() 103 104 if not patient.connected: 105 gmDispatcher.send(signal = 'statustext', msg = _('Cannot edit progress notes. No active patient.')) 106 return False 107 108 if parent is None: 109 parent = wx.GetApp().GetTopWindow() 110 111 emr = patient.get_emr() 112 #-------------------------- 113 def delete(item): 114 if item is None: 115 return False 116 dlg = gmGuiHelpers.c2ButtonQuestionDlg ( 117 parent, 118 -1, 119 caption = _('Deleting progress note'), 120 question = _( 121 'Are you positively sure you want to delete this\n' 122 'progress note from the medical record ?\n' 123 '\n' 124 'Note that even if you chose to delete the entry it will\n' 125 'still be (invisibly) kept in the audit trail to protect\n' 126 'you from litigation because physical deletion is known\n' 127 'to be unlawful in some jurisdictions.\n' 128 ), 129 button_defs = ( 130 {'label': _('Delete'), 'tooltip': _('Yes, delete the progress note.'), 'default': False}, 131 {'label': _('Cancel'), 'tooltip': _('No, do NOT delete the progress note.'), 'default': True} 132 ) 133 ) 134 decision = dlg.ShowModal() 135 136 if decision != wx.ID_YES: 137 return False 138 139 gmClinNarrative.delete_clin_narrative(narrative = item['pk_narrative']) 140 return True
141 #-------------------------- 142 def edit(item): 143 if item is None: 144 return False 145 146 dlg = gmGuiHelpers.cMultilineTextEntryDlg ( 147 parent, 148 -1, 149 title = _('Editing progress note'), 150 msg = _( 151 'This is the original progress note:\n' 152 '\n' 153 ' %s' 154 ) % item.format(left_margin = u' ', fancy = True), 155 text = item['narrative'] 156 ) 157 decision = dlg.ShowModal() 158 159 if decision != wx.ID_SAVE: 160 return False 161 162 val = dlg.value 163 dlg.Destroy() 164 if val.strip() == u'': 165 return False 166 167 item['narrative'] = val 168 item.save_payload() 169 170 return True 171 #-------------------------- 172 def refresh(lctrl): 173 notes = emr.get_clin_narrative ( 174 encounters = encounters, 175 episodes = episodes, 176 providers = [ gmPerson.gmCurrentProvider()['short_alias'] ] 177 ) 178 lctrl.set_string_items(items = [ 179 [ narr['date'].strftime('%x %H:%M'), 180 gmClinNarrative.soap_cat2l10n[narr['soap_cat']], 181 narr['narrative'].replace('\n', '/').replace('\r', '/') 182 ] for narr in notes 183 ]) 184 lctrl.set_data(data = notes) 185 #-------------------------- 186 187 gmListWidgets.get_choices_from_list ( 188 parent = parent, 189 caption = _('Managing progress notes'), 190 msg = _( 191 '\n' 192 ' This list shows the progress notes by %s.\n' 193 '\n' 194 ) % gmPerson.gmCurrentProvider()['short_alias'], 195 columns = [_('when'), _('type'), _('entry')], 196 single_selection = True, 197 can_return_empty = False, 198 edit_callback = edit, 199 delete_callback = delete, 200 refresh_callback = refresh 201 ) 202 #------------------------------------------------------------
203 -def search_narrative_across_emrs(parent=None):
204 205 if parent is None: 206 parent = wx.GetApp().GetTopWindow() 207 208 searcher = wx.TextEntryDialog ( 209 parent = parent, 210 message = _('Enter (regex) term to search for across all EMRs:'), 211 caption = _('Text search across all EMRs'), 212 style = wx.OK | wx.CANCEL | wx.CENTRE 213 ) 214 result = searcher.ShowModal() 215 216 if result != wx.ID_OK: 217 return 218 219 wx.BeginBusyCursor() 220 term = searcher.GetValue() 221 searcher.Destroy() 222 results = gmClinNarrative.search_text_across_emrs(search_term = term) 223 wx.EndBusyCursor() 224 225 if len(results) == 0: 226 gmGuiHelpers.gm_show_info ( 227 _( 228 'Nothing found for search term:\n' 229 ' "%s"' 230 ) % term, 231 _('Search results') 232 ) 233 return 234 235 items = [ [gmPerson.cIdentity(aPK_obj = r['pk_patient'])['description_gender'], r['narrative'], r['src_table']] for r in results ] 236 237 selected_patient = gmListWidgets.get_choices_from_list ( 238 parent = parent, 239 caption = _('Search results for %s') % term, 240 choices = items, 241 columns = [_('Patient'), _('Match'), _('Match location')], 242 data = [ r['pk_patient'] for r in results ], 243 single_selection = True, 244 can_return_empty = False 245 ) 246 247 if selected_patient is None: 248 return 249 250 wx.CallAfter(gmPatSearchWidgets.set_active_patient, patient = gmPerson.cIdentity(aPK_obj = selected_patient))
251 #------------------------------------------------------------
252 -def search_narrative_in_emr(parent=None, patient=None):
253 254 # sanity checks 255 if patient is None: 256 patient = gmPerson.gmCurrentPatient() 257 258 if not patient.connected: 259 gmDispatcher.send(signal = 'statustext', msg = _('Cannot search EMR. No active patient.')) 260 return False 261 262 if parent is None: 263 parent = wx.GetApp().GetTopWindow() 264 265 searcher = wx.TextEntryDialog ( 266 parent = parent, 267 message = _('Enter search term:'), 268 caption = _('Text search of entire EMR of active patient'), 269 style = wx.OK | wx.CANCEL | wx.CENTRE 270 ) 271 result = searcher.ShowModal() 272 273 if result != wx.ID_OK: 274 searcher.Destroy() 275 return False 276 277 wx.BeginBusyCursor() 278 val = searcher.GetValue() 279 searcher.Destroy() 280 emr = patient.get_emr() 281 rows = emr.search_narrative_simple(val) 282 wx.EndBusyCursor() 283 284 if len(rows) == 0: 285 gmGuiHelpers.gm_show_info ( 286 _( 287 'Nothing found for search term:\n' 288 ' "%s"' 289 ) % val, 290 _('Search results') 291 ) 292 return True 293 294 txt = u'' 295 for row in rows: 296 txt += u'%s: %s\n' % ( 297 row['soap_cat'], 298 row['narrative'] 299 ) 300 301 txt += u' %s: %s - %s %s\n' % ( 302 _('Encounter'), 303 row['encounter_started'].strftime('%x %H:%M'), 304 row['encounter_ended'].strftime('%H:%M'), 305 row['encounter_type'] 306 ) 307 txt += u' %s: %s\n' % ( 308 _('Episode'), 309 row['episode'] 310 ) 311 txt += u' %s: %s\n\n' % ( 312 _('Health issue'), 313 row['health_issue'] 314 ) 315 316 msg = _( 317 'Search term was: "%s"\n' 318 '\n' 319 'Search results:\n\n' 320 '%s\n' 321 ) % (val, txt) 322 323 dlg = wx.MessageDialog ( 324 parent = parent, 325 message = msg, 326 caption = _('Search results for %s') % val, 327 style = wx.OK | wx.STAY_ON_TOP 328 ) 329 dlg.ShowModal() 330 dlg.Destroy() 331 332 return True
333 #------------------------------------------------------------
334 -def export_narrative_for_medistar_import(parent=None, soap_cats=u'soap', encounter=None):
335 336 # sanity checks 337 pat = gmPerson.gmCurrentPatient() 338 if not pat.connected: 339 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR for Medistar. No active patient.')) 340 return False 341 342 if encounter is None: 343 encounter = pat.get_emr().active_encounter 344 345 if parent is None: 346 parent = wx.GetApp().GetTopWindow() 347 348 # get file name 349 aWildcard = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files")) 350 # FIXME: make configurable 351 aDefDir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed','export'))) 352 # FIXME: make configurable 353 fname = '%s-%s-%s-%s-%s.txt' % ( 354 'Medistar-MD', 355 time.strftime('%Y-%m-%d',time.localtime()), 356 pat['lastnames'].replace(' ', '-'), 357 pat['firstnames'].replace(' ', '_'), 358 pat.get_formatted_dob(format = '%Y-%m-%d') 359 ) 360 dlg = wx.FileDialog ( 361 parent = parent, 362 message = _("Save EMR extract for MEDISTAR import as..."), 363 defaultDir = aDefDir, 364 defaultFile = fname, 365 wildcard = aWildcard, 366 style = wx.SAVE 367 ) 368 choice = dlg.ShowModal() 369 fname = dlg.GetPath() 370 dlg.Destroy() 371 if choice != wx.ID_OK: 372 return False 373 374 wx.BeginBusyCursor() 375 _log.debug('exporting encounter for medistar import to [%s]', fname) 376 exporter = gmPatientExporter.cMedistarSOAPExporter() 377 successful, fname = exporter.export_to_file ( 378 filename = fname, 379 encounter = encounter, 380 soap_cats = u'soap', 381 export_to_import_file = True 382 ) 383 if not successful: 384 gmGuiHelpers.gm_show_error ( 385 _('Error exporting progress notes for MEDISTAR import.'), 386 _('MEDISTAR progress notes export') 387 ) 388 wx.EndBusyCursor() 389 return False 390 391 gmDispatcher.send(signal = 'statustext', msg = _('Successfully exported progress notes into file [%s] for Medistar import.') % fname, beep=False) 392 393 wx.EndBusyCursor() 394 return True
395 #------------------------------------------------------------
396 -def select_narrative_from_episodes(parent=None, soap_cats=None):
397 """soap_cats needs to be a list""" 398 399 pat = gmPerson.gmCurrentPatient() 400 emr = pat.get_emr() 401 402 if parent is None: 403 parent = wx.GetApp().GetTopWindow() 404 405 selected_soap = {} 406 selected_issue_pks = [] 407 selected_episode_pks = [] 408 selected_narrative_pks = [] 409 410 while 1: 411 # 1) select health issues to select episodes from 412 all_issues = emr.get_health_issues() 413 all_issues.insert(0, gmEMRStructItems.get_dummy_health_issue()) 414 dlg = gmEMRStructWidgets.cIssueListSelectorDlg ( 415 parent = parent, 416 id = -1, 417 issues = all_issues, 418 msg = _('\n In the list below mark the health issues you want to report on.\n') 419 ) 420 selection_idxs = [] 421 for idx in range(len(all_issues)): 422 if all_issues[idx]['pk_health_issue'] in selected_issue_pks: 423 selection_idxs.append(idx) 424 if len(selection_idxs) != 0: 425 dlg.set_selections(selections = selection_idxs) 426 btn_pressed = dlg.ShowModal() 427 selected_issues = dlg.get_selected_item_data() 428 dlg.Destroy() 429 430 if btn_pressed == wx.ID_CANCEL: 431 return selected_soap.values() 432 433 selected_issue_pks = [ i['pk_health_issue'] for i in selected_issues ] 434 435 while 1: 436 # 2) select episodes to select items from 437 all_epis = emr.get_episodes(issues = selected_issue_pks) 438 439 if len(all_epis) == 0: 440 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.')) 441 break 442 443 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg ( 444 parent = parent, 445 id = -1, 446 episodes = all_epis, 447 msg = _( 448 '\n These are the episodes known for the health issues just selected.\n\n' 449 ' Now, mark the the episodes you want to report on.\n' 450 ) 451 ) 452 selection_idxs = [] 453 for idx in range(len(all_epis)): 454 if all_epis[idx]['pk_episode'] in selected_episode_pks: 455 selection_idxs.append(idx) 456 if len(selection_idxs) != 0: 457 dlg.set_selections(selections = selection_idxs) 458 btn_pressed = dlg.ShowModal() 459 selected_epis = dlg.get_selected_item_data() 460 dlg.Destroy() 461 462 if btn_pressed == wx.ID_CANCEL: 463 break 464 465 selected_episode_pks = [ i['pk_episode'] for i in selected_epis ] 466 467 # 3) select narrative corresponding to the above constraints 468 all_narr = emr.get_clin_narrative(episodes = selected_episode_pks, soap_cats = soap_cats) 469 470 if len(all_narr) == 0: 471 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episodes.')) 472 continue 473 474 dlg = cNarrativeListSelectorDlg ( 475 parent = parent, 476 id = -1, 477 narrative = all_narr, 478 msg = _( 479 '\n This is the narrative (type %s) for the chosen episodes.\n\n' 480 ' Now, mark the entries you want to include in your report.\n' 481 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soap')) ]) 482 ) 483 selection_idxs = [] 484 for idx in range(len(all_narr)): 485 if all_narr[idx]['pk_narrative'] in selected_narrative_pks: 486 selection_idxs.append(idx) 487 if len(selection_idxs) != 0: 488 dlg.set_selections(selections = selection_idxs) 489 btn_pressed = dlg.ShowModal() 490 selected_narr = dlg.get_selected_item_data() 491 dlg.Destroy() 492 493 if btn_pressed == wx.ID_CANCEL: 494 continue 495 496 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ] 497 for narr in selected_narr: 498 selected_soap[narr['pk_narrative']] = narr
499 #------------------------------------------------------------
500 -class cNarrativeListSelectorDlg(gmListWidgets.cGenericListSelectorDlg):
501
502 - def __init__(self, *args, **kwargs):
503 504 narrative = kwargs['narrative'] 505 del kwargs['narrative'] 506 507 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs) 508 509 self.SetTitle(_('Select the narrative you are interested in ...')) 510 # FIXME: add epi/issue 511 self._LCTRL_items.set_columns([_('when'), _('who'), _('type'), _('entry')]) #, _('Episode'), u'', _('Health Issue')]) 512 # FIXME: date used should be date of encounter, not date_modified 513 self._LCTRL_items.set_string_items ( 514 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 ] 515 ) 516 self._LCTRL_items.set_column_widths() 517 self._LCTRL_items.set_data(data = narrative)
518 #------------------------------------------------------------
519 -class cMoveNarrativeDlg(wxgMoveNarrativeDlg.wxgMoveNarrativeDlg):
520
521 - def __init__(self, *args, **kwargs):
522 523 self.encounter = kwargs['encounter'] 524 self.source_episode = kwargs['episode'] 525 del kwargs['encounter'] 526 del kwargs['episode'] 527 528 wxgMoveNarrativeDlg.wxgMoveNarrativeDlg.__init__(self, *args, **kwargs) 529 530 self.LBL_source_episode.SetLabel(u'%s%s' % (self.source_episode['description'], gmTools.coalesce(self.source_episode['health_issue'], u'', u' (%s)'))) 531 self.LBL_encounter.SetLabel('%s: %s %s - %s' % ( 532 self.encounter['started'].strftime('%x').decode(gmI18N.get_encoding()), 533 self.encounter['l10n_type'], 534 self.encounter['started'].strftime('%H:%M'), 535 self.encounter['last_affirmed'].strftime('%H:%M') 536 )) 537 pat = gmPerson.gmCurrentPatient() 538 emr = pat.get_emr() 539 narr = emr.get_clin_narrative(episodes=[self.source_episode['pk_episode']], encounters=[self.encounter['pk_encounter']]) 540 if len(narr) == 0: 541 narr = [{'narrative': _('There is no narrative for this episode in this encounter.')}] 542 self.LBL_narrative.SetLabel(u'\n'.join([n['narrative'] for n in narr]))
543 544 #------------------------------------------------------------
545 - def _on_move_button_pressed(self, event):
546 547 target_episode = self._PRW_episode_selector.GetData(can_create = False) 548 549 if target_episode is None: 550 gmDispatcher.send(signal='statustext', msg=_('Must select episode to move narrative to first.')) 551 # FIXME: set to pink 552 self._PRW_episode_selector.SetFocus() 553 return False 554 555 target_episode = gmEMRStructItems.cEpisode(aPK_obj=target_episode) 556 557 self.encounter.transfer_clinical_data ( 558 source_episode = self.source_episode, 559 target_episode = target_episode 560 ) 561 562 if self.IsModal(): 563 self.EndModal(wx.ID_OK) 564 else: 565 self.Close()
566 #============================================================ 567 from Gnumed.wxGladeWidgets import wxgSoapPluginPnl 568
569 -class cSoapPluginPnl(wxgSoapPluginPnl.wxgSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
570 """A panel for in-context editing of progress notes. 571 572 Expects to be used as a notebook page. 573 574 Left hand side: 575 - problem list (health issues and active episodes) 576 - hints area 577 578 Right hand side: 579 - previous notes 580 - notebook with progress note editors 581 - encounter details fields 582 583 Listens to patient change signals, thus acts on the current patient. 584 """
585 - def __init__(self, *args, **kwargs):
586 587 wxgSoapPluginPnl.wxgSoapPluginPnl.__init__(self, *args, **kwargs) 588 gmRegetMixin.cRegetOnPaintMixin.__init__(self) 589 590 self.__pat = gmPerson.gmCurrentPatient() 591 self.__init_ui() 592 self.__reset_ui_content() 593 594 self.__register_interests()
595 #-------------------------------------------------------- 596 # public API 597 #--------------------------------------------------------
598 - def save_encounter(self):
599 600 if not self.__encounter_valid_for_save(): 601 return False 602 603 emr = self.__pat.get_emr() 604 enc = emr.active_encounter 605 606 enc['pk_type'] = self._PRW_encounter_type.GetData() 607 enc['started'] = self._PRW_encounter_start.GetData().get_pydt() 608 enc['last_affirmed'] = self._PRW_encounter_end.GetData().get_pydt() 609 rfe = self._TCTRL_rfe.GetValue().strip() 610 if len(rfe) == 0: 611 enc['reason_for_encounter'] = None 612 else: 613 enc['reason_for_encounter'] = rfe 614 aoe = self._TCTRL_aoe.GetValue().strip() 615 if len(aoe) == 0: 616 enc['assessment_of_encounter'] = None 617 else: 618 enc['assessment_of_encounter'] = aoe 619 620 enc.save_payload() 621 622 return True
623 #-------------------------------------------------------- 624 # internal helpers 625 #--------------------------------------------------------
626 - def __init_ui(self):
627 self._LCTRL_active_problems.set_columns([_('Last'), _('Problem'), _('Health issue')]) 628 self._LCTRL_active_problems.set_string_items() 629 630 self._splitter_main.SetSashGravity(0.5) 631 self._splitter_left.SetSashGravity(0.5) 632 self._splitter_right.SetSashGravity(1.0) 633 634 splitter_size = self._splitter_main.GetSizeTuple()[0] 635 self._splitter_main.SetSashPosition(splitter_size * 3 / 10, True) 636 637 splitter_size = self._splitter_left.GetSizeTuple()[1] 638 self._splitter_left.SetSashPosition(splitter_size * 6 / 20, True) 639 640 splitter_size = self._splitter_right.GetSizeTuple()[1] 641 self._splitter_right.SetSashPosition(splitter_size * 15 / 20, True) 642 643 self._NB_soap_editors.DeleteAllPages()
644 #--------------------------------------------------------
645 - def __reset_ui_content(self):
646 """ 647 Clear all information from input panel 648 """ 649 self._LCTRL_active_problems.set_string_items() 650 self._lbl_hints.SetLabel(u'') 651 self._TCTRL_recent_notes.SetValue(u'') 652 self._NB_soap_editors.DeleteAllPages() 653 self._NB_soap_editors.add_editor() 654 self._PRW_encounter_type.SetText(suppress_smarts = True) 655 self._PRW_encounter_start.SetText(suppress_smarts = True) 656 self._PRW_encounter_end.SetText(suppress_smarts = True) 657 self._TCTRL_rfe.SetValue(u'') 658 self._TCTRL_aoe.SetValue(u'')
659 #--------------------------------------------------------
660 - def __refresh_problem_list(self):
661 """Update health problems list. 662 """ 663 664 self._LCTRL_active_problems.set_string_items() 665 666 emr = self.__pat.get_emr() 667 problems = emr.get_problems ( 668 include_closed_episodes = self._CHBOX_show_closed_episodes.IsChecked(), 669 include_irrelevant_issues = self._CHBOX_irrelevant_issues.IsChecked() 670 ) 671 672 list_items = [] 673 active_problems = [] 674 for problem in problems: 675 if not problem['problem_active']: 676 if not problem['is_potential_problem']: 677 continue 678 679 active_problems.append(problem) 680 681 if problem['type'] == 'issue': 682 issue = emr.problem2issue(problem) 683 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue']) 684 if last_encounter is None: 685 last = issue['modified_when'].strftime('%m/%Y') 686 else: 687 last = last_encounter['last_affirmed'].strftime('%m/%Y') 688 689 list_items.append([last, problem['problem'], gmTools.u_left_arrow]) 690 691 elif problem['type'] == 'episode': 692 epi = emr.problem2episode(problem) 693 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode']) 694 if last_encounter is None: 695 last = epi['episode_modified_when'].strftime('%m/%Y') 696 else: 697 last = last_encounter['last_affirmed'].strftime('%m/%Y') 698 699 list_items.append ([ 700 last, 701 problem['problem'], 702 gmTools.coalesce(initial = epi['health_issue'], instead = gmTools.u_diameter) 703 ]) 704 705 self._LCTRL_active_problems.set_string_items(items = list_items) 706 self._LCTRL_active_problems.set_column_widths() 707 self._LCTRL_active_problems.set_data(data = active_problems) 708 709 showing_potential_problems = ( 710 self._CHBOX_show_closed_episodes.IsChecked() 711 or 712 self._CHBOX_irrelevant_issues.IsChecked() 713 ) 714 if showing_potential_problems: 715 self._SZR_problem_list_staticbox.SetLabel(_('%s (active+potential) problems') % len(list_items)) 716 else: 717 self._SZR_problem_list_staticbox.SetLabel(_('%s active problems') % len(list_items)) 718 719 return True
720 #--------------------------------------------------------
721 - def __refresh_recent_notes(self, problem=None):
722 """This refreshes the recent-notes part.""" 723 724 if problem is None: 725 soap = u'' 726 caption = u'<?>' 727 728 elif problem['type'] == u'issue': 729 emr = self.__pat.get_emr() 730 soap = u'' 731 caption = problem['problem'][:35] 732 733 prev_enc = emr.get_last_but_one_encounter(issue_id = problem['pk_health_issue']) 734 if prev_enc is not None: 735 soap += prev_enc.format ( 736 with_soap = True, 737 with_docs = False, 738 with_tests = False, 739 patient = self.__pat, 740 issues = [ problem['pk_health_issue'] ], 741 fancy_header = False 742 ) 743 744 tmp = emr.active_encounter.format_soap ( 745 soap_cats = 'soap', 746 emr = emr, 747 issues = [ problem['pk_health_issue'] ], 748 ) 749 if len(tmp) > 0: 750 soap += _('Current encounter:') + u'\n' 751 soap += u'\n'.join(tmp) + u'\n' 752 753 elif problem['type'] == u'episode': 754 emr = self.__pat.get_emr() 755 soap = u'' 756 caption = problem['problem'][:35] 757 758 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_episode']) 759 if prev_enc is None: 760 if problem['pk_health_issue'] is not None: 761 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_health_issue']) 762 if prev_enc is not None: 763 soap += prev_enc.format ( 764 with_soap = True, 765 with_docs = False, 766 with_tests = False, 767 patient = self.__pat, 768 issues = [ problem['pk_health_issue'] ], 769 fancy_header = False 770 ) 771 else: 772 soap += prev_enc.format ( 773 episodes = [ problem['pk_episode'] ], 774 with_soap = True, 775 with_docs = False, 776 with_tests = False, 777 patient = self.__pat, 778 fancy_header = False 779 ) 780 781 tmp = emr.active_encounter.format_soap ( 782 soap_cats = 'soap', 783 emr = emr, 784 issues = [ problem['pk_health_issue'] ], 785 ) 786 if len(tmp) > 0: 787 soap += _('Current encounter:') + u'\n' 788 soap += u'\n'.join(tmp) + u'\n' 789 790 else: 791 soap = u'' 792 caption = u'<?>' 793 794 self._TCTRL_recent_notes.SetValue(soap) 795 self._TCTRL_recent_notes.ShowPosition(self._TCTRL_recent_notes.GetLastPosition()) 796 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on %s%s%s') % ( 797 gmTools.u_left_double_angle_quote, 798 caption, 799 gmTools.u_right_double_angle_quote 800 )) 801 802 self._TCTRL_recent_notes.Refresh() 803 804 return True
805 #--------------------------------------------------------
806 - def __refresh_encounter(self):
807 """Update encounter fields. 808 """ 809 emr = self.__pat.get_emr() 810 enc = emr.active_encounter 811 self._PRW_encounter_type.SetText(value = enc['l10n_type'], data = enc['pk_type']) 812 813 fts = gmDateTime.cFuzzyTimestamp ( 814 timestamp = enc['started'], 815 accuracy = gmDateTime.acc_minutes 816 ) 817 self._PRW_encounter_start.SetText(fts.format_accurately(), data=fts) 818 819 fts = gmDateTime.cFuzzyTimestamp ( 820 timestamp = enc['last_affirmed'], 821 accuracy = gmDateTime.acc_minutes 822 ) 823 self._PRW_encounter_end.SetText(fts.format_accurately(), data=fts) 824 825 self._TCTRL_rfe.SetValue(gmTools.coalesce(enc['reason_for_encounter'], u'')) 826 self._TCTRL_aoe.SetValue(gmTools.coalesce(enc['assessment_of_encounter'], u'')) 827 828 self._PRW_encounter_type.Refresh() 829 self._PRW_encounter_start.Refresh() 830 self._PRW_encounter_end.Refresh() 831 self._TCTRL_rfe.Refresh() 832 self._TCTRL_aoe.Refresh()
833 #--------------------------------------------------------
834 - def __encounter_modified(self):
835 """Assumes that the field data is valid.""" 836 837 emr = self.__pat.get_emr() 838 enc = emr.active_encounter 839 840 data = { 841 'pk_type': self._PRW_encounter_type.GetData(), 842 'reason_for_encounter': gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u''), 843 'assessment_of_encounter': gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''), 844 'pk_location': enc['pk_location'] 845 } 846 847 if self._PRW_encounter_start.GetData() is None: 848 data['started'] = None 849 else: 850 data['started'] = self._PRW_encounter_start.GetData().get_pydt() 851 852 if self._PRW_encounter_end.GetData() is None: 853 data['last_affirmed'] = None 854 else: 855 data['last_affirmed'] = self._PRW_encounter_end.GetData().get_pydt() 856 857 return enc.same_payload(another_object = data)
858 #--------------------------------------------------------
859 - def __encounter_valid_for_save(self):
860 861 found_error = False 862 863 if self._PRW_encounter_type.GetData() is None: 864 found_error = True 865 msg = _('Cannot save encounter: missing type.') 866 867 if self._PRW_encounter_start.GetData() is None: 868 found_error = True 869 msg = _('Cannot save encounter: missing start time.') 870 871 if self._PRW_encounter_end.GetData() is None: 872 found_error = True 873 msg = _('Cannot save encounter: missing end time.') 874 875 if found_error: 876 gmDispatcher.send(signal = 'statustext', msg = msg, beep = True) 877 return False 878 879 return True
880 #-------------------------------------------------------- 881 # event handling 882 #--------------------------------------------------------
883 - def __register_interests(self):
884 """Configure enabled event signals.""" 885 # client internal signals 886 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection) 887 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection) 888 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db) 889 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db) 890 gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified) 891 gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_modified) 892 893 # synchronous signals 894 self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback) 895 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
896 #--------------------------------------------------------
897 - def _pre_selection_callback(self):
898 """Another patient is about to be activated. 899 900 Patient change will not proceed before this returns True. 901 """ 902 # don't worry about the encounter here - it will be offered 903 # for editing higher up if anything was saved to the EMR 904 if not self.__pat.connected: 905 return True 906 return self._NB_soap_editors.warn_on_unsaved_soap()
907 #--------------------------------------------------------
908 - def _pre_exit_callback(self):
909 """The client is about to be shut down. 910 911 Shutdown will not proceed before this returns. 912 """ 913 if not self.__pat.connected: 914 return True 915 916 # if self.__encounter_modified(): 917 # do_save_enc = gmGuiHelpers.gm_show_question ( 918 # aMessage = _( 919 # 'You have modified the details\n' 920 # 'of the current encounter.\n' 921 # '\n' 922 # 'Do you want to save those changes ?' 923 # ), 924 # aTitle = _('Starting new encounter') 925 # ) 926 # if do_save_enc: 927 # if not self.save_encounter(): 928 # gmDispatcher.send(signal = u'statustext', msg = _('Error saving current encounter.'), beep = True) 929 930 emr = self.__pat.get_emr() 931 if not self._NB_soap_editors.save_all_editors(emr = emr, rfe = self._TCTRL_rfe.GetValue().strip(), aoe = self._TCTRL_aoe.GetValue().strip()): 932 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True) 933 return True
934 #--------------------------------------------------------
935 - def _on_pre_patient_selection(self):
936 wx.CallAfter(self.__on_pre_patient_selection)
937 #--------------------------------------------------------
938 - def __on_pre_patient_selection(self):
939 self.__reset_ui_content()
940 #--------------------------------------------------------
941 - def _on_post_patient_selection(self):
942 wx.CallAfter(self._schedule_data_reget)
943 #--------------------------------------------------------
944 - def _on_episode_issue_mod_db(self):
945 wx.CallAfter(self._schedule_data_reget)
946 #--------------------------------------------------------
948 wx.CallAfter(self.__refresh_encounter)
949 #--------------------------------------------------------
950 - def _on_problem_focused(self, event):
951 """Show related note at the bottom.""" 952 pass
953 #--------------------------------------------------------
954 - def _on_problem_selected(self, event):
955 """Show related note at the bottom.""" 956 emr = self.__pat.get_emr() 957 self.__refresh_recent_notes ( 958 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True) 959 )
960 #--------------------------------------------------------
961 - def _on_problem_activated(self, event):
962 """Open progress note editor for this problem. 963 """ 964 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True) 965 if problem is None: 966 return True 967 968 dbcfg = gmCfg.cCfgSQL() 969 allow_duplicate_editors = bool(dbcfg.get2 ( 970 option = u'horstspace.soap_editor.allow_same_episode_multiple_times', 971 workplace = gmSurgery.gmCurrentPractice().active_workplace, 972 bias = u'user', 973 default = False 974 )) 975 if self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors): 976 return True 977 978 gmGuiHelpers.gm_show_error ( 979 aMessage = _( 980 'Cannot open progress note editor for\n\n' 981 '[%s].\n\n' 982 ) % problem['problem'], 983 aTitle = _('opening progress note editor') 984 ) 985 event.Skip() 986 return False
987 #--------------------------------------------------------
988 - def _on_discard_editor_button_pressed(self, event):
989 self._NB_soap_editors.close_current_editor() 990 event.Skip()
991 #--------------------------------------------------------
992 - def _on_new_editor_button_pressed(self, event):
993 self._NB_soap_editors.add_editor() 994 event.Skip()
995 #--------------------------------------------------------
996 - def _on_clear_editor_button_pressed(self, event):
997 self._NB_soap_editors.clear_current_editor() 998 event.Skip()
999 #--------------------------------------------------------
1000 - def _on_save_all_button_pressed(self, event):
1001 self.save_encounter() 1002 emr = self.__pat.get_emr() 1003 if not self._NB_soap_editors.save_all_editors(emr = emr, rfe = self._TCTRL_rfe.GetValue().strip(), aoe = self._TCTRL_aoe.GetValue().strip()): 1004 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True) 1005 event.Skip()
1006 #--------------------------------------------------------
1007 - def _on_save_encounter_button_pressed(self, event):
1008 self.save_encounter() 1009 event.Skip()
1010 #--------------------------------------------------------
1011 - def _on_save_note_button_pressed(self, event):
1012 emr = self.__pat.get_emr() 1013 self._NB_soap_editors.save_current_editor ( 1014 emr = emr, 1015 rfe = self._TCTRL_rfe.GetValue().strip(), 1016 aoe = self._TCTRL_aoe.GetValue().strip() 1017 ) 1018 event.Skip()
1019 #--------------------------------------------------------
1020 - def _on_new_encounter_button_pressed(self, event):
1021 1022 if self.__encounter_modified(): 1023 do_save_enc = gmGuiHelpers.gm_show_question ( 1024 aMessage = _( 1025 'You have modified the details\n' 1026 'of the current encounter.\n' 1027 '\n' 1028 'Do you want to save those changes ?' 1029 ), 1030 aTitle = _('Starting new encounter') 1031 ) 1032 if do_save_enc: 1033 if not self.save_encounter(): 1034 gmDispatcher.send(signal = u'statustext', msg = _('Error saving current encounter.'), beep = True) 1035 return False 1036 1037 emr = self.__pat.get_emr() 1038 gmDispatcher.send(signal = u'statustext', msg = _('Started new encounter for active patient.'), beep = True) 1039 1040 event.Skip() 1041 1042 wx.CallAfter(gmEMRStructWidgets.start_new_encounter, emr = emr)
1043 #--------------------------------------------------------
1044 - def _on_show_closed_episodes_checked(self, event):
1045 self.__refresh_problem_list()
1046 #--------------------------------------------------------
1047 - def _on_irrelevant_issues_checked(self, event):
1048 self.__refresh_problem_list()
1049 #-------------------------------------------------------- 1050 # reget mixin API 1051 #--------------------------------------------------------
1052 - def _populate_with_data(self):
1053 self.__refresh_problem_list() 1054 self.__refresh_encounter() 1055 return True
1056 #============================================================
1057 -class cSoapNoteInputNotebook(wx.Notebook):
1058 """A notebook holding panels with progress note editors. 1059 1060 There can be one or several progress note editor panel 1061 for each episode being worked on. The editor class in 1062 each panel is configurable. 1063 1064 There will always be one open editor. 1065 """
1066 - def __init__(self, *args, **kwargs):
1067 1068 kwargs['style'] = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER 1069 1070 wx.Notebook.__init__(self, *args, **kwargs)
1071 #-------------------------------------------------------- 1072 # public API 1073 #--------------------------------------------------------
1074 - def add_editor(self, problem=None, allow_same_problem=False):
1075 """Add a progress note editor page. 1076 1077 The way <allow_same_problem> is currently used in callers 1078 it only applies to unassociated episodes. 1079 """ 1080 problem_to_add = problem 1081 1082 # determine label 1083 if problem_to_add is None: 1084 label = _('new problem') 1085 else: 1086 # normalize problem type 1087 if isinstance(problem_to_add, gmEMRStructItems.cEpisode): 1088 problem_to_add = gmEMRStructItems.episode2problem(episode = problem_to_add) 1089 1090 elif isinstance(problem_to_add, gmEMRStructItems.cHealthIssue): 1091 problem_to_add = gmEMRStructItems.health_issue2problem(episode = problem_to_add) 1092 1093 if not isinstance(problem_to_add, gmEMRStructItems.cProblem): 1094 raise TypeError('cannot open progress note editor for [%s]' % problem_to_add) 1095 1096 label = problem_to_add['problem'] 1097 # FIXME: configure maximum length 1098 if len(label) > 23: 1099 label = label[:21] + gmTools.u_ellipsis 1100 1101 # new unassociated problem or dupes allowed 1102 if (problem_to_add is None) or allow_same_problem: 1103 new_page = cSoapNoteExpandoEditAreaPnl(parent = self, id = -1, problem = problem_to_add) 1104 result = self.AddPage ( 1105 page = new_page, 1106 text = label, 1107 select = True 1108 ) 1109 return result 1110 1111 # real problem, no dupes allowed 1112 # - raise existing editor 1113 for page_idx in range(self.GetPageCount()): 1114 page = self.GetPage(page_idx) 1115 1116 # editor is for unassociated new problem 1117 if page.problem is None: 1118 continue 1119 1120 # editor is for episode 1121 if page.problem['type'] == 'episode': 1122 if page.problem['pk_episode'] == problem_to_add['pk_episode']: 1123 self.SetSelection(page_idx) 1124 gmDispatcher.send(signal = u'statustext', msg = u'Raising existing editor.', beep = True) 1125 return True 1126 continue 1127 1128 # editor is for health issue 1129 if page.problem['type'] == 'issue': 1130 if page.problem['pk_health_issue'] == problem_to_add['pk_health_issue']: 1131 self.SetSelection(page_idx) 1132 gmDispatcher.send(signal = u'statustext', msg = u'Raising existing editor.', beep = True) 1133 return True 1134 continue 1135 1136 # - or add new editor 1137 new_page = cSoapNoteExpandoEditAreaPnl(parent = self, id = -1, problem = problem_to_add) 1138 result = self.AddPage ( 1139 page = new_page, 1140 text = label, 1141 select = True 1142 ) 1143 1144 return result
1145 #--------------------------------------------------------
1146 - def close_current_editor(self):
1147 1148 page_idx = self.GetSelection() 1149 page = self.GetPage(page_idx) 1150 1151 if not page.empty: 1152 really_discard = gmGuiHelpers.gm_show_question ( 1153 _('Are you sure you really want to\n' 1154 'discard this progress note ?\n' 1155 ), 1156 _('Discarding progress note') 1157 ) 1158 if really_discard is False: 1159 return 1160 1161 self.DeletePage(page_idx) 1162 1163 # always keep one unassociated editor open 1164 if self.GetPageCount() == 0: 1165 self.add_editor()
1166 #--------------------------------------------------------
1167 - def save_current_editor(self, emr=None, rfe=None, aoe=None):
1168 1169 page_idx = self.GetSelection() 1170 page = self.GetPage(page_idx) 1171 1172 if not page.save(emr = emr, rfe = rfe, aoe = aoe): 1173 return 1174 1175 self.DeletePage(page_idx) 1176 1177 # always keep one unassociated editor open 1178 if self.GetPageCount() == 0: 1179 self.add_editor()
1180 #--------------------------------------------------------
1181 - def warn_on_unsaved_soap(self):
1182 for page_idx in range(self.GetPageCount()): 1183 page = self.GetPage(page_idx) 1184 if page.empty: 1185 continue 1186 1187 gmGuiHelpers.gm_show_warning ( 1188 _('There are unsaved progress notes !\n'), 1189 _('Unsaved progress notes') 1190 ) 1191 return False 1192 1193 return True
1194 #--------------------------------------------------------
1195 - def save_all_editors(self, emr=None, rfe=None, aoe=None):
1196 1197 all_closed = True 1198 for page_idx in range(self.GetPageCount()): 1199 page = self.GetPage(page_idx) 1200 if page.save(emr = emr, rfe = rfe, aoe = aoe): 1201 self.DeletePage(page_idx) 1202 else: 1203 all_closed = False 1204 1205 # always keep one unassociated editor open 1206 if self.GetPageCount() == 0: 1207 self.add_editor() 1208 1209 return (all_closed is True)
1210 #--------------------------------------------------------
1211 - def clear_current_editor(self):
1212 page_idx = self.GetSelection() 1213 page = self.GetPage(page_idx) 1214 page.clear()
1215 #--------------------------------------------------------
1216 - def get_current_problem(self):
1217 page_idx = self.GetSelection() 1218 page = self.GetPage(page_idx) 1219 return page.problem
1220 #============================================================
1221 -class cSoapNoteExpandoEditAreaPnl(wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl):
1222
1223 - def __init__(self, *args, **kwargs):
1224 1225 try: 1226 self.problem = kwargs['problem'] 1227 del kwargs['problem'] 1228 except KeyError: 1229 self.problem = None 1230 1231 wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl.__init__(self, *args, **kwargs) 1232 1233 self.fields = [ 1234 self._TCTRL_Soap, 1235 self._TCTRL_sOap, 1236 self._TCTRL_soAp, 1237 self._TCTRL_soaP 1238 ] 1239 1240 self.__register_interests()
1241 #--------------------------------------------------------
1242 - def clear(self):
1243 for field in self.fields: 1244 field.SetValue(u'')
1245 #--------------------------------------------------------
1246 - def save(self, emr=None, rfe=None, aoe=None):
1247 1248 if self.empty: 1249 return True 1250 1251 # new unassociated episode 1252 if (self.problem is None) or (self.problem['type'] == 'issue'): 1253 1254 epi_name = gmTools.coalesce ( 1255 aoe, 1256 gmTools.coalesce ( 1257 rfe, 1258 u'' 1259 ) 1260 ).strip().replace('\r', '//').replace('\n', '//') 1261 1262 dlg = wx.TextEntryDialog ( 1263 parent = self, 1264 message = _('Enter a short working name for this new problem:'), 1265 caption = _('Creating a problem (episode) to save the notelet under ...'), 1266 defaultValue = epi_name, 1267 style = wx.OK | wx.CANCEL | wx.CENTRE 1268 ) 1269 decision = dlg.ShowModal() 1270 if decision != wx.ID_OK: 1271 return False 1272 1273 epi_name = dlg.GetValue().strip() 1274 if epi_name == u'': 1275 gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note')) 1276 return False 1277 1278 # create episode 1279 new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True) 1280 1281 if self.problem is not None: 1282 issue = emr.problem2issue(self.problem) 1283 if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True): 1284 gmGuiHelpers.gm_show_warning ( 1285 _( 1286 'The new episode:\n' 1287 '\n' 1288 ' "%s"\n' 1289 '\n' 1290 'will remain unassociated despite the editor\n' 1291 'having been invoked from the health issue:\n' 1292 '\n' 1293 ' "%s"' 1294 ) % ( 1295 new_episode['description'], 1296 issue['description'] 1297 ), 1298 _('saving progress note') 1299 ) 1300 1301 epi_id = new_episode['pk_episode'] 1302 else: 1303 epi_id = self.problem['pk_episode'] 1304 1305 emr.add_notes(notes = self.soap, episode = epi_id) 1306 1307 return True
1308 #-------------------------------------------------------- 1309 # event handling 1310 #--------------------------------------------------------
1311 - def __register_interests(self):
1312 for field in self.fields: 1313 wxexpando.EVT_ETC_LAYOUT_NEEDED(field, field.GetId(), self._on_expando_needs_layout)
1314 #--------------------------------------------------------
1315 - def _on_expando_needs_layout(self, evt):
1316 # need to tell ourselves to re-Layout to refresh scroll bars 1317 1318 # provoke adding scrollbar if needed 1319 self.Fit() 1320 1321 if self.HasScrollbar(wx.VERTICAL): 1322 # scroll panel to show cursor 1323 expando = self.FindWindowById(evt.GetId()) 1324 y_expando = expando.GetPositionTuple()[1] 1325 h_expando = expando.GetSizeTuple()[1] 1326 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1 1327 y_cursor = int(round((float(line_cursor) / expando.NumberOfLines) * h_expando)) 1328 y_desired_visible = y_expando + y_cursor 1329 1330 y_view = self.ViewStart[1] 1331 h_view = self.GetClientSizeTuple()[1] 1332 1333 # print "expando:", y_expando, "->", h_expando, ", lines:", expando.NumberOfLines 1334 # print "cursor :", y_cursor, "at line", line_cursor, ", insertion point:", expando.GetInsertionPoint() 1335 # print "wanted :", y_desired_visible 1336 # print "view-y :", y_view 1337 # print "scroll2:", h_view 1338 1339 # expando starts before view 1340 if y_desired_visible < y_view: 1341 # print "need to scroll up" 1342 self.Scroll(0, y_desired_visible) 1343 1344 if y_desired_visible > h_view: 1345 # print "need to scroll down" 1346 self.Scroll(0, y_desired_visible)
1347 #-------------------------------------------------------- 1348 # properties 1349 #--------------------------------------------------------
1350 - def _get_soap(self):
1351 note = [] 1352 1353 tmp = self._TCTRL_Soap.GetValue().strip() 1354 if tmp != u'': 1355 note.append(['s', tmp]) 1356 1357 tmp = self._TCTRL_sOap.GetValue().strip() 1358 if tmp != u'': 1359 note.append(['o', tmp]) 1360 1361 tmp = self._TCTRL_soAp.GetValue().strip() 1362 if tmp != u'': 1363 note.append(['a', tmp]) 1364 1365 tmp = self._TCTRL_soaP.GetValue().strip() 1366 if tmp != u'': 1367 note.append(['p', tmp]) 1368 1369 return note
1370 1371 soap = property(_get_soap, lambda x:x) 1372 #--------------------------------------------------------
1373 - def _get_empty(self):
1374 for field in self.fields: 1375 if field.GetValue().strip() != u'': 1376 return False 1377 return True
1378 1379 empty = property(_get_empty, lambda x:x)
1380 #============================================================
1381 -class cSoapLineTextCtrl(wxexpando.ExpandoTextCtrl):
1382
1383 - def __init__(self, *args, **kwargs):
1384 1385 wxexpando.ExpandoTextCtrl.__init__(self, *args, **kwargs) 1386 1387 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+") 1388 1389 self.__register_interests()
1390 #------------------------------------------------ 1391 # event handling 1392 #------------------------------------------------
1393 - def __register_interests(self):
1394 #wx.EVT_KEY_DOWN (self, self.__on_key_down) 1395 #wx.EVT_KEY_UP (self, self.__OnKeyUp) 1396 wx.EVT_CHAR(self, self.__on_char) 1397 wx.EVT_SET_FOCUS(self, self.__on_focus)
1398 #--------------------------------------------------------
1399 - def __on_focus(self, evt):
1400 evt.Skip() 1401 wx.CallAfter(self._after_on_focus)
1402 #--------------------------------------------------------
1403 - def _after_on_focus(self):
1404 evt = wx.PyCommandEvent(wxexpando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId()) 1405 evt.SetEventObject(self) 1406 evt.height = None 1407 evt.numLines = None 1408 self.GetEventHandler().ProcessEvent(evt)
1409 #--------------------------------------------------------
1410 - def __on_char(self, evt):
1411 char = unichr(evt.GetUnicodeKey()) 1412 1413 if self.LastPosition == 1: 1414 evt.Skip() 1415 return 1416 1417 explicit_expansion = False 1418 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT): # portable CTRL-ALT-... 1419 if evt.GetKeyCode() != 13: 1420 evt.Skip() 1421 return 1422 explicit_expansion = True 1423 1424 if not explicit_expansion: 1425 if self.__keyword_separators.match(char) is None: 1426 evt.Skip() 1427 return 1428 1429 caret_pos, line_no = self.PositionToXY(self.InsertionPoint) 1430 line = self.GetLineText(line_no) 1431 word = self.__keyword_separators.split(line[:caret_pos])[-1] 1432 1433 if ( 1434 (not explicit_expansion) 1435 and 1436 (word != u'$$steffi') # Easter Egg ;-) 1437 and 1438 (word not in [ r[0] for r in gmPG2.get_text_expansion_keywords() ]) 1439 ): 1440 evt.Skip() 1441 return 1442 1443 start = self.InsertionPoint - len(word) 1444 wx.CallAfter(self.replace_keyword_with_expansion, word, start, explicit_expansion) 1445 1446 evt.Skip() 1447 return
1448 #------------------------------------------------
1449 - def replace_keyword_with_expansion(self, keyword=None, position=None, show_list=False):
1450 1451 if show_list: 1452 candidates = gmPG2.get_keyword_expansion_candidates(keyword = keyword) 1453 if len(candidates) == 0: 1454 return 1455 if len(candidates) == 1: 1456 keyword = candidates[0] 1457 else: 1458 keyword = gmListWidgets.get_choices_from_list ( 1459 parent = self, 1460 msg = _( 1461 'Several macros match the keyword [%s].\n' 1462 '\n' 1463 'Please select the expansion you want to happen.' 1464 ) % keyword, 1465 caption = _('Selecting text macro'), 1466 choices = candidates, 1467 columns = [_('Keyword')], 1468 single_selection = True, 1469 can_return_empty = False 1470 ) 1471 if keyword is None: 1472 return 1473 1474 expansion = gmPG2.expand_keyword(keyword = keyword) 1475 1476 if expansion is None: 1477 return 1478 1479 if expansion == u'': 1480 return 1481 1482 self.Replace ( 1483 position, 1484 position + len(keyword), 1485 expansion 1486 ) 1487 1488 self.SetInsertionPoint(position + len(expansion) + 1) 1489 self.ShowPosition(position + len(expansion) + 1) 1490 1491 return
1492 #============================================================ 1493 # main 1494 #------------------------------------------------------------ 1495 if __name__ == '__main__': 1496 1497 gmI18N.activate_locale() 1498 gmI18N.install_domain(domain = 'gnumed') 1499 1500 #----------------------------------------
1501 - def test_select_narrative_from_episodes():
1502 pat = gmPerson.ask_for_patient() 1503 gmPatSearchWidgets.set_active_patient(patient = pat) 1504 app = wx.PyWidgetTester(size = (200, 200)) 1505 sels = select_narrative_from_episodes() 1506 print "selected:" 1507 for sel in sels: 1508 print sel
1509 #----------------------------------------
1510 - def test_cSoapNoteExpandoEditAreaPnl():
1511 pat = gmPerson.ask_for_patient() 1512 application = wx.PyWidgetTester(size=(800,500)) 1513 soap_input = cSoapNoteExpandoEditAreaPnl(application.frame, -1) 1514 application.frame.Show(True) 1515 application.MainLoop()
1516 #----------------------------------------
1517 - def test_cSoapPluginPnl():
1518 patient = gmPerson.ask_for_patient() 1519 if patient is None: 1520 print "No patient. Exiting gracefully..." 1521 return 1522 gmPatSearchWidgets.set_active_patient(patient=patient) 1523 1524 application = wx.PyWidgetTester(size=(800,500)) 1525 soap_input = cSoapPluginPnl(application.frame, -1) 1526 application.frame.Show(True) 1527 soap_input._schedule_data_reget() 1528 application.MainLoop()
1529 #---------------------------------------- 1530 if (len(sys.argv) > 1) and (sys.argv[1] == 'test'): 1531 #test_select_narrative_from_episodes() 1532 test_cSoapNoteExpandoEditAreaPnl() 1533 #test_cSoapPluginPnl() 1534 1535 #============================================================ 1536 # $Log: gmNarrativeWidgets.py,v $ 1537 # Revision 1.45 2010/01/11 19:51:09 ncq 1538 # - cleanup 1539 # - warn-on-unsaved-soap and use in syn pre-selection callback 1540 # 1541 # Revision 1.44 2009/11/28 18:32:50 ncq 1542 # - finalize showing potential problems in problem list, too, and adjust box label 1543 # 1544 # Revision 1.43 2009/11/24 21:03:41 ncq 1545 # - display problems based on checkbox selection 1546 # - set recent notes label based on problem selection 1547 # 1548 # Revision 1.42 2009/11/15 01:10:09 ncq 1549 # - enhance move-progress-notes-to-another-encounter 1550 # - use enhanced new encounter start 1551 # 1552 # Revision 1.41 2009/11/13 21:08:24 ncq 1553 # - enable cross-EMR narrative search to activate matching 1554 # patient from result list 1555 # 1556 # Revision 1.40 2009/11/08 20:49:49 ncq 1557 # - implement search across all EMRs 1558 # 1559 # Revision 1.39 2009/09/13 18:45:25 ncq 1560 # - no more get-active-encounter() 1561 # 1562 # Revision 1.38 2009/09/01 22:36:59 ncq 1563 # - wx-CallAfter on start-new-encounter 1564 # 1565 # Revision 1.37 2009/07/23 16:41:13 ncq 1566 # - cleanup 1567 # 1568 # Revision 1.36 2009/07/02 20:55:48 ncq 1569 # - properly honor allow-same-problem on non-new editors only 1570 # 1571 # Revision 1.35 2009/07/01 17:09:06 ncq 1572 # - refresh fields explicitly when active encounter is switched 1573 # 1574 # Revision 1.34 2009/06/29 15:09:45 ncq 1575 # - inform user when nothing is found during search 1576 # - refresh recent-notes on problem single-click selection 1577 # but NOT anymore on editor changes 1578 # 1579 # Revision 1.33 2009/06/22 09:28:20 ncq 1580 # - improved wording as per list 1581 # 1582 # Revision 1.32 2009/06/20 22:39:27 ncq 1583 # - improved wording as per list discussion 1584 # 1585 # Revision 1.31 2009/06/11 12:37:25 ncq 1586 # - much simplified initial setup of list ctrls 1587 # 1588 # Revision 1.30 2009/06/04 16:33:13 ncq 1589 # - adjust to dob-less person 1590 # - use set-active-patient from pat-search-widgets 1591 # 1592 # Revision 1.29 2009/05/13 13:12:41 ncq 1593 # - cleanup 1594 # 1595 # Revision 1.28 2009/05/13 12:22:05 ncq 1596 # - move_progress_notes_to_another_encounter 1597 # 1598 # Revision 1.27 2009/04/16 12:51:02 ncq 1599 # - edit_* -> manage_progress_notes as it can delete now, too, 1600 # after being converted to using get_choices_from_list 1601 # 1602 # Revision 1.26 2009/04/13 10:56:21 ncq 1603 # - use same_payload on encounter to detect changes 1604 # - detect when current encounter is switched, not just modified 1605 # 1606 # Revision 1.25 2009/03/10 14:23:56 ncq 1607 # - comment 1608 # 1609 # Revision 1.24 2009/03/02 18:57:52 ncq 1610 # - make expando soap editor scroll to cursor when needed 1611 # 1612 # Revision 1.23 2009/02/24 13:22:06 ncq 1613 # - fix saving edited progress notes 1614 # 1615 # Revision 1.22 2009/02/17 08:07:37 ncq 1616 # - support explicit macro expansion 1617 # 1618 # Revision 1.21 2009/01/21 22:37:14 ncq 1619 # - do not fail save_all_editors() where not appropriate 1620 # 1621 # Revision 1.20 2009/01/21 18:53:57 ncq 1622 # - fix save_all_editors and call it with proper args 1623 # 1624 # Revision 1.19 2009/01/03 17:29:01 ncq 1625 # - listen on new current_encounter_modified 1626 # - detecting encounter field changes at exit/patient doesn't properly work 1627 # - refresh recent notes where needed 1628 # 1629 # Revision 1.18 2009/01/02 11:41:16 ncq 1630 # - improved event handling 1631 # 1632 # Revision 1.17 2008/12/27 15:50:41 ncq 1633 # - almost finish implementing soap saving 1634 # 1635 # Revision 1.16 2008/12/26 22:35:44 ncq 1636 # - edit_progress_notes 1637 # - implement most of new soap plugin functionality 1638 # 1639 # Revision 1.15 2008/11/24 11:10:29 ncq 1640 # - cleanup 1641 # 1642 # Revision 1.14 2008/11/23 12:47:02 ncq 1643 # - preset splitter ratios and gravity 1644 # - cleanup 1645 # - reorder recent notes with most recent on bottom as per list 1646 # 1647 # Revision 1.13 2008/11/20 20:35:50 ncq 1648 # - new soap plugin widgets 1649 # 1650 # Revision 1.12 2008/10/26 01:21:52 ncq 1651 # - factor out searching EMR for narrative 1652 # 1653 # Revision 1.11 2008/10/22 12:21:57 ncq 1654 # - use %x in strftime where appropriate 1655 # 1656 # Revision 1.10 2008/10/12 16:26:20 ncq 1657 # - consultation -> encounter 1658 # 1659 # Revision 1.9 2008/09/02 19:01:12 ncq 1660 # - adjust to clin health_issue fk_patient drop and related changes 1661 # 1662 # Revision 1.8 2008/07/28 15:46:05 ncq 1663 # - export_narrative_for_medistar_import 1664 # 1665 # Revision 1.7 2008/03/05 22:30:14 ncq 1666 # - new style logging 1667 # 1668 # Revision 1.6 2007/12/03 20:45:28 ncq 1669 # - improved docs 1670 # 1671 # Revision 1.5 2007/09/10 12:36:02 ncq 1672 # - improved wording in narrative selector at SOAP level 1673 # 1674 # Revision 1.4 2007/09/09 19:21:04 ncq 1675 # - get top level wx.App window if parent is None 1676 # - support filtering by soap_cats 1677 # 1678 # Revision 1.3 2007/09/07 22:45:58 ncq 1679 # - much improved select_narrative_from_episodes() 1680 # 1681 # Revision 1.2 2007/09/07 10:59:17 ncq 1682 # - greatly improve select_narrative_by_episodes 1683 # - remember selections 1684 # - properly levelled looping 1685 # - fix test suite 1686 # 1687 # Revision 1.1 2007/08/29 22:06:15 ncq 1688 # - factored out narrative widgets 1689 # 1690 # 1691