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