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

Source Code for Module Gnumed.wxpython.gmNarrativeWidgets

   1  """GNUmed narrative handling widgets.""" 
   2  #================================================================ 
   3  __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>" 
   4  __license__ = "GPL v2 or later (details at http://www.gnu.org)" 
   5   
   6  import sys 
   7  import logging 
   8  import os.path 
   9  import time 
  10   
  11   
  12  import wx 
  13   
  14   
  15  if __name__ == '__main__': 
  16          sys.path.insert(0, '../../') 
  17   
  18  from Gnumed.pycommon import gmI18N 
  19   
  20  if __name__ == '__main__': 
  21          gmI18N.activate_locale() 
  22          gmI18N.install_domain() 
  23   
  24  from Gnumed.pycommon import gmDispatcher 
  25  from Gnumed.pycommon import gmTools 
  26  from Gnumed.pycommon import gmDateTime 
  27  from Gnumed.pycommon import gmCfg 
  28   
  29  from Gnumed.business import gmPerson 
  30  from Gnumed.business import gmStaff 
  31  from Gnumed.business import gmEMRStructItems 
  32  from Gnumed.business import gmSoapDefs 
  33  from Gnumed.business import gmPraxis 
  34  from Gnumed.business import gmPersonSearch 
  35   
  36  from Gnumed.wxpython import gmListWidgets 
  37  from Gnumed.wxpython import gmEMRStructWidgets 
  38  from Gnumed.wxpython import gmEncounterWidgets 
  39  from Gnumed.wxpython import gmRegetMixin 
  40  from Gnumed.wxpython import gmGuiHelpers 
  41  from Gnumed.wxpython import gmVisualProgressNoteWidgets 
  42  from Gnumed.wxpython import gmProgressNotesEAWidgets 
  43  from Gnumed.wxpython.gmPatSearchWidgets import set_active_patient 
  44   
  45  from Gnumed.exporters import gmPatientExporter 
  46   
  47   
  48  _log = logging.getLogger('gm.ui') 
  49  #============================================================ 
  50  # narrative related widgets 
  51  #------------------------------------------------------------ 
52 -class cNarrativeListSelectorDlg(gmListWidgets.cGenericListSelectorDlg):
53
54 - def __init__(self, *args, **kwargs):
55 56 narrative = kwargs['narrative'] 57 del kwargs['narrative'] 58 59 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs) 60 61 self.SetTitle(_('Select the narrative you are interested in ...')) 62 # FIXME: add epi/issue 63 self._LCTRL_items.set_columns([_('when'), _('who'), _('type'), _('entry')]) #, _('Episode'), u'', _('Health Issue')]) 64 # FIXME: date used should be date of encounter, not date_modified 65 self._LCTRL_items.set_string_items ( 66 items = [ [narr['date'].strftime('%x %H:%M'), narr['modified_by'], gmSoapDefs.soap_cat2l10n[narr['soap_cat']], narr['narrative'].replace('\n', '/').replace('\r', '/')] for narr in narrative ] 67 ) 68 self._LCTRL_items.set_column_widths() 69 self._LCTRL_items.set_data(data = narrative)
70 71 #------------------------------------------------------------ 72 from Gnumed.wxGladeWidgets import wxgMoveNarrativeDlg 73
74 -class cMoveNarrativeDlg(wxgMoveNarrativeDlg.wxgMoveNarrativeDlg):
75
76 - def __init__(self, *args, **kwargs):
77 78 self.encounter = kwargs['encounter'] 79 self.source_episode = kwargs['episode'] 80 del kwargs['encounter'] 81 del kwargs['episode'] 82 83 wxgMoveNarrativeDlg.wxgMoveNarrativeDlg.__init__(self, *args, **kwargs) 84 85 self.LBL_source_episode.SetLabel('%s%s' % (self.source_episode['description'], gmTools.coalesce(self.source_episode['health_issue'], '', ' (%s)'))) 86 self.LBL_encounter.SetLabel('%s: %s %s - %s' % ( 87 gmDateTime.pydt_strftime(self.encounter['started'], '%Y %b %d'), 88 self.encounter['l10n_type'], 89 gmDateTime.pydt_strftime(self.encounter['started'], '%H:%M'), 90 gmDateTime.pydt_strftime(self.encounter['last_affirmed'], '%H:%M') 91 )) 92 pat = gmPerson.gmCurrentPatient() 93 emr = pat.emr 94 narr = emr.get_clin_narrative(episodes=[self.source_episode['pk_episode']], encounters=[self.encounter['pk_encounter']]) 95 if len(narr) == 0: 96 narr = [{'narrative': _('There is no narrative for this episode in this encounter.')}] 97 self.LBL_narrative.SetLabel('\n'.join([n['narrative'] for n in narr]))
98 99 #------------------------------------------------------------
100 - def _on_move_button_pressed(self, event):
101 102 target_episode = self._PRW_episode_selector.GetData(can_create = False) 103 104 if target_episode is None: 105 gmDispatcher.send(signal='statustext', msg=_('Must select episode to move narrative to first.')) 106 # FIXME: set to pink 107 self._PRW_episode_selector.SetFocus() 108 return False 109 110 target_episode = gmEMRStructItems.cEpisode(aPK_obj=target_episode) 111 112 self.encounter.transfer_clinical_data ( 113 source_episode = self.source_episode, 114 target_episode = target_episode 115 ) 116 117 if self.IsModal(): 118 self.EndModal(wx.ID_OK) 119 else: 120 self.Close()
121 122 #============================================================ 123 #============================================================ 124 from Gnumed.wxGladeWidgets import wxgSoapPluginPnl 125
126 -class cSoapPluginPnl(wxgSoapPluginPnl.wxgSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
127 """A panel for in-context editing of progress notes. 128 129 Expects to be used as a notebook page. 130 131 Left hand side: 132 - problem list (health issues and active episodes) 133 - previous notes 134 135 Right hand side: 136 - panel handling 137 - encounter details fields 138 - notebook with progress note editors 139 - visual progress notes 140 141 Listens to patient change signals, thus acts on the current patient. 142 """
143 - def __init__(self, *args, **kwargs):
144 145 wxgSoapPluginPnl.wxgSoapPluginPnl.__init__(self, *args, **kwargs) 146 gmRegetMixin.cRegetOnPaintMixin.__init__(self) 147 148 self.__pat = gmPerson.gmCurrentPatient() 149 self.__init_ui() 150 self.__reset_ui_content() 151 self.__register_interests()
152 #-------------------------------------------------------- 153 # internal helpers 154 #--------------------------------------------------------
155 - def __init_ui(self):
156 self._LCTRL_active_problems.set_columns([_('Last'), _('Problem'), _('In health issue')]) 157 self._LCTRL_active_problems.set_string_items() 158 self._LCTRL_active_problems.extend_popup_menu_callback = self._extend_popup_menu 159 160 self._splitter_main.SetSashGravity(0.5) 161 self._splitter_left.SetSashGravity(0.5) 162 163 splitter_size = self._splitter_main.GetSize()[0] 164 self._splitter_main.SetSashPosition(splitter_size * 3 // 10, True) 165 166 splitter_size = self._splitter_left.GetSize()[1] 167 self._splitter_left.SetSashPosition(splitter_size * 6 // 20, True)
168 169 #--------------------------------------------------------
170 - def _extend_popup_menu(self, menu=None):
171 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True) 172 if problem is None: 173 return 174 self.__focussed_problem = problem 175 176 menu_item = menu.Append(-1, _('Edit')) 177 if self.__focussed_problem['type'] == 'issue': 178 self.Bind(wx.EVT_MENU, self._on_edit_issue, menu_item) 179 if self.__focussed_problem['type'] == 'episode': 180 self.Bind(wx.EVT_MENU, self._on_edit_episode, menu_item)
181 182 #--------------------------------------------------------
183 - def __reset_ui_content(self):
184 """Clear all information from input panel.""" 185 186 self._LCTRL_active_problems.set_string_items() 187 188 self._TCTRL_recent_notes.SetValue('') 189 self._SZR_recent_notes.StaticBox.SetLabel(_('Most recent notes on selected problem')) 190 191 self._PNL_editors.patient = None
192 #--------------------------------------------------------
193 - def __refresh_problem_list(self):
194 """Update health problems list.""" 195 196 self._LCTRL_active_problems.set_string_items() 197 198 emr = self.__pat.emr 199 problems = emr.get_problems ( 200 include_closed_episodes = self._CHBOX_show_closed_episodes.IsChecked(), 201 include_irrelevant_issues = self._CHBOX_irrelevant_issues.IsChecked() 202 ) 203 204 list_items = [] 205 active_problems = [] 206 for problem in problems: 207 if not problem['problem_active']: 208 if not problem['is_potential_problem']: 209 continue 210 211 active_problems.append(problem) 212 213 if problem['type'] == 'issue': 214 issue = emr.problem2issue(problem) 215 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue']) 216 if last_encounter is None: 217 last = issue['modified_when'].strftime('%m/%Y') 218 else: 219 last = last_encounter['last_affirmed'].strftime('%m/%Y') 220 221 list_items.append([last, problem['problem'], gmTools.u_left_arrow_with_tail]) 222 223 elif problem['type'] == 'episode': 224 epi = emr.problem2episode(problem) 225 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode']) 226 if last_encounter is None: 227 last = epi['episode_modified_when'].strftime('%m/%Y') 228 else: 229 last = last_encounter['last_affirmed'].strftime('%m/%Y') 230 231 list_items.append ([ 232 last, 233 problem['problem'], 234 gmTools.coalesce(value2test = epi['health_issue'], return_instead = '?') #gmTools.u_diameter 235 ]) 236 237 self._LCTRL_active_problems.set_string_items(items = list_items) 238 self._LCTRL_active_problems.set_column_widths() 239 self._LCTRL_active_problems.set_data(data = active_problems) 240 241 showing_potential_problems = ( 242 self._CHBOX_show_closed_episodes.IsChecked() 243 or 244 self._CHBOX_irrelevant_issues.IsChecked() 245 ) 246 if showing_potential_problems: 247 self._SZR_problem_list.StaticBox.SetLabel(_('%s (active+potential) problems') % len(list_items)) 248 else: 249 self._SZR_problem_list.StaticBox.SetLabel(_('%s active problems') % len(list_items)) 250 251 return True
252 #--------------------------------------------------------
253 - def __get_info_for_issue_problem(self, problem=None, fancy=False):
254 soap = '' 255 emr = self.__pat.emr 256 prev_enc = emr.get_last_but_one_encounter(issue_id = problem['pk_health_issue']) 257 if prev_enc is not None: 258 soap += prev_enc.format ( 259 issues = [ problem['pk_health_issue'] ], 260 with_soap = True, 261 with_docs = fancy, 262 with_tests = fancy, 263 patient = self.__pat, 264 fancy_header = False, 265 with_rfe_aoe = True 266 ) 267 268 tmp = emr.active_encounter.format_soap ( 269 soap_cats = 'soapu', 270 emr = emr, 271 issues = [ problem['pk_health_issue'] ], 272 ) 273 if len(tmp) > 0: 274 soap += _('Current encounter:') + '\n' 275 soap += '\n'.join(tmp) + '\n' 276 277 if problem['summary'] is not None: 278 soap += '\n-- %s ----------\n%s' % ( 279 _('Cumulative summary'), 280 gmTools.wrap ( 281 text = problem['summary'], 282 width = 45, 283 initial_indent = ' ', 284 subsequent_indent = ' ' 285 ).strip('\n') 286 ) 287 288 return soap
289 #--------------------------------------------------------
290 - def __get_info_for_episode_problem(self, problem=None, fancy=False):
291 soap = '' 292 emr = self.__pat.emr 293 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_episode']) 294 if prev_enc is not None: 295 soap += prev_enc.format ( 296 episodes = [ problem['pk_episode'] ], 297 with_soap = True, 298 with_docs = fancy, 299 with_tests = fancy, 300 patient = self.__pat, 301 fancy_header = False, 302 with_rfe_aoe = True 303 ) 304 else: 305 if problem['pk_health_issue'] is not None: 306 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_health_issue']) 307 if prev_enc is not None: 308 soap += prev_enc.format ( 309 with_soap = True, 310 with_docs = fancy, 311 with_tests = fancy, 312 patient = self.__pat, 313 issues = [ problem['pk_health_issue'] ], 314 fancy_header = False, 315 with_rfe_aoe = True 316 ) 317 318 if problem['pk_health_issue'] is None: 319 tmp = emr.active_encounter.format_soap(soap_cats = 'soapu', emr = emr) 320 else: 321 tmp = emr.active_encounter.format_soap(soap_cats = 'soapu', emr = emr, issues = [problem['pk_health_issue']]) 322 if len(tmp) > 0: 323 soap += _('Current encounter:') + '\n' 324 soap += '\n'.join(tmp) + '\n' 325 326 if problem['summary'] is not None: 327 soap += '\n-- %s ----------\n%s' % ( 328 _('Cumulative summary'), 329 gmTools.wrap ( 330 text = problem['summary'], 331 width = 45, 332 initial_indent = ' ', 333 subsequent_indent = ' ' 334 ).strip('\n') 335 ) 336 337 return soap
338 #--------------------------------------------------------
339 - def __refresh_recent_notes(self, problem=None):
340 """This refreshes the recent-notes part.""" 341 342 if problem is None: 343 caption = '<?>' 344 txt = '' 345 elif problem['type'] == 'issue': 346 caption = problem['problem'][:35] 347 txt = self.__get_info_for_issue_problem(problem = problem, fancy = not self._RBTN_notes_only.GetValue()) 348 elif problem['type'] == 'episode': 349 caption = problem['problem'][:35] 350 txt = self.__get_info_for_episode_problem(problem = problem, fancy = not self._RBTN_notes_only.GetValue()) 351 352 self._TCTRL_recent_notes.SetValue(txt) 353 self._TCTRL_recent_notes.ShowPosition(self._TCTRL_recent_notes.GetLastPosition()) 354 self._SZR_recent_notes.StaticBox.SetLabel(_('Most recent info on %s%s%s') % ( 355 gmTools.u_left_double_angle_quote, 356 caption, 357 gmTools.u_right_double_angle_quote 358 )) 359 360 self._TCTRL_recent_notes.Refresh() 361 362 return True
363 #-------------------------------------------------------- 364 # event handling 365 #--------------------------------------------------------
366 - def __register_interests(self):
367 """Configure enabled event signals.""" 368 # client internal signals 369 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection) 370 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection) 371 gmDispatcher.connect(signal = 'clin.episode_mod_db', receiver = self._on_episode_issue_mod_db) 372 gmDispatcher.connect(signal = 'clin.health_issue_mod_db', receiver = self._on_episode_issue_mod_db) 373 gmDispatcher.connect(signal = 'clin.episode_code_mod_db', receiver = self._on_episode_issue_mod_db)
374 #--------------------------------------------------------
376 self.__reset_ui_content()
377 #--------------------------------------------------------
378 - def _on_post_patient_selection(self):
379 self._schedule_data_reget() 380 self._PNL_editors.patient = self.__pat
381 #--------------------------------------------------------
382 - def _on_episode_issue_mod_db(self):
383 self._schedule_data_reget()
384 #-------------------------------------------------------- 385 # problem list specific events 386 #--------------------------------------------------------
387 - def _on_problem_focused(self, event):
388 """Show related note at the bottom.""" 389 pass
390 #--------------------------------------------------------
391 - def _on_edit_issue(self, evt):
392 gmEMRStructWidgets.edit_health_issue(parent = self, issue = self.__focussed_problem.get_as_health_issue())
393 394 #--------------------------------------------------------
395 - def _on_edit_episode(self, evt):
396 gmEMRStructWidgets.edit_episode(parent = self, episode = self.__focussed_problem.get_as_episode())
397 398 #--------------------------------------------------------
399 - def _on_problem_selected(self, event):
400 """Show related note at the bottom.""" 401 self.__refresh_recent_notes ( 402 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True) 403 )
404 #--------------------------------------------------------
405 - def _on_problem_activated(self, event):
406 """Open progress note editor for this problem. 407 """ 408 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True) 409 if problem is None: 410 return True 411 412 dbcfg = gmCfg.cCfgSQL() 413 allow_duplicate_editors = bool(dbcfg.get2 ( 414 option = 'horstspace.soap_editor.allow_same_episode_multiple_times', 415 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 416 bias = 'user', 417 default = False 418 )) 419 if self._PNL_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors): 420 return True 421 422 gmGuiHelpers.gm_show_error ( 423 aMessage = _( 424 'Cannot open progress note editor for\n\n' 425 '[%s].\n\n' 426 ) % problem['problem'], 427 aTitle = _('opening progress note editor') 428 ) 429 return False
430 #--------------------------------------------------------
431 - def _on_show_closed_episodes_checked(self, event):
432 self.__refresh_problem_list()
433 #--------------------------------------------------------
434 - def _on_irrelevant_issues_checked(self, event):
435 self.__refresh_problem_list()
436 #-------------------------------------------------------- 437 # recent-notes specific events 438 #--------------------------------------------------------
439 - def _on_notes_only_selected(self, event):
440 self.__refresh_recent_notes ( 441 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True) 442 )
443 #--------------------------------------------------------
444 - def _on_full_encounter_selected(self, event):
445 self.__refresh_recent_notes ( 446 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True) 447 )
448 #-------------------------------------------------------- 449 # reget mixin API 450 #-------------------------------------------------------- 451 # only needed for debugging: 452 #def _schedule_data_reget(self): 453 # gmRegetMixin.cRegetOnPaintMixin._schedule_data_reget(self) 454 #--------------------------------------------------------
455 - def _populate_with_data(self):
456 self.__refresh_problem_list() 457 return True
458 459 #============================================================ 460 from Gnumed.wxGladeWidgets import wxgFancySoapEditorPnl 461
462 -class cFancySoapEditorPnl(wxgFancySoapEditorPnl.wxgFancySoapEditorPnl):
463 """A panel holding everything needed to edit in context: 464 465 - encounter metadata 466 - progress notes 467 - textual 468 - visual 469 - episode summary 470 471 Does NOT act on the current patient. 472 """
473 - def __init__(self, *args, **kwargs):
474 475 wxgFancySoapEditorPnl.wxgFancySoapEditorPnl.__init__(self, *args, **kwargs) 476 477 self.__init_ui() 478 self.patient = None 479 self.__register_interests()
480 #-------------------------------------------------------- 481 # public API 482 #--------------------------------------------------------
483 - def add_editor(self, problem=None, allow_same_problem=False):
484 return self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_same_problem)
485 #--------------------------------------------------------
486 - def _get_patient(self):
487 return self.__pat
488
489 - def _set_patient(self, patient):
490 #self.__pat.register_before_switching_from_patient_callback(callback = self._before_switching_from_patient_callback) 491 self.__pat = patient 492 self.__refresh_encounter() 493 self.__refresh_soap_notebook()
494 495 patient = property(_get_patient, _set_patient) 496 #--------------------------------------------------------
497 - def save_encounter(self):
498 499 if self.__pat is None: 500 return True 501 502 if not self.__encounter_valid_for_save(): 503 return False 504 505 enc = self.__pat.emr.active_encounter 506 507 rfe = self._TCTRL_rfe.GetValue().strip() 508 if len(rfe) == 0: 509 enc['reason_for_encounter'] = None 510 else: 511 enc['reason_for_encounter'] = rfe 512 aoe = self._TCTRL_aoe.GetValue().strip() 513 if len(aoe) == 0: 514 enc['assessment_of_encounter'] = None 515 else: 516 enc['assessment_of_encounter'] = aoe 517 518 enc.save_payload() 519 520 enc.generic_codes_rfe = [ c['data'] for c in self._PRW_rfe_codes.GetData() ] 521 enc.generic_codes_aoe = [ c['data'] for c in self._PRW_aoe_codes.GetData() ] 522 523 return True
524 #-------------------------------------------------------- 525 # internal helpers 526 #--------------------------------------------------------
527 - def __init_ui(self):
528 self._NB_soap_editors.MoveAfterInTabOrder(self._PRW_aoe_codes)
529 #--------------------------------------------------------
530 - def __reset_soap_notebook(self):
531 self._NB_soap_editors.DeleteAllPages() 532 self._NB_soap_editors.add_editor()
533 #--------------------------------------------------------
534 - def __refresh_soap_notebook(self):
535 self.__reset_soap_notebook() 536 537 if self.__pat is None: 538 return 539 540 dbcfg = gmCfg.cCfgSQL() 541 auto_open_recent_problems = bool(dbcfg.get2 ( 542 option = 'horstspace.soap_editor.auto_open_latest_episodes', 543 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 544 bias = 'user', 545 default = True 546 )) 547 548 emr = self.__pat.emr 549 recent_epis = emr.active_encounter.get_episodes() 550 prev_enc = emr.get_last_but_one_encounter() 551 if prev_enc is not None: 552 recent_epis.extend(prev_enc.get_episodes()) 553 554 for epi in recent_epis: 555 if not epi['episode_open']: 556 continue 557 self._NB_soap_editors.add_editor(problem = epi)
558 559 #--------------------------------------------------------
560 - def __reset_encounter_fields(self):
561 self._TCTRL_rfe.SetValue('') 562 self._PRW_rfe_codes.SetText(suppress_smarts = True) 563 self._TCTRL_aoe.SetValue('') 564 self._PRW_aoe_codes.SetText(suppress_smarts = True)
565 566 #--------------------------------------------------------
567 - def __refresh_encounter(self):
568 """Update encounter fields.""" 569 570 self.__reset_encounter_fields() 571 572 if self.__pat is None: 573 return 574 575 enc = self.__pat.emr.active_encounter 576 577 self._TCTRL_rfe.SetValue(gmTools.coalesce(enc['reason_for_encounter'], '')) 578 val, data = self._PRW_rfe_codes.generic_linked_codes2item_dict(enc.generic_codes_rfe) 579 self._PRW_rfe_codes.SetText(val, data) 580 581 self._TCTRL_aoe.SetValue(gmTools.coalesce(enc['assessment_of_encounter'], '')) 582 val, data = self._PRW_aoe_codes.generic_linked_codes2item_dict(enc.generic_codes_aoe) 583 self._PRW_aoe_codes.SetText(val, data) 584 585 self._TCTRL_rfe.Refresh() 586 self._PRW_rfe_codes.Refresh() 587 self._TCTRL_aoe.Refresh() 588 self._PRW_aoe_codes.Refresh()
589 590 #--------------------------------------------------------
591 - def __refresh_current_editor(self):
592 self._NB_soap_editors.refresh_current_editor()
593 594 # #-------------------------------------------------------- 595 # def __encounter_modified(self): 596 # """Assumes that the field data is valid.""" 597 # 598 # emr = self.__pat.emr 599 # enc = emr.active_encounter 600 # 601 # data = { 602 # 'pk_type': enc['pk_type'], 603 # 'reason_for_encounter': gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u''), 604 # 'assessment_of_encounter': gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''), 605 # 'pk_location': enc['pk_org_unit'], 606 # 'pk_patient': enc['pk_patient'], 607 # 'pk_generic_codes_rfe': self._PRW_rfe_codes.GetData(), 608 # 'pk_generic_codes_aoe': self._PRW_aoe_codes.GetData(), 609 # 'started': enc['started'], 610 # 'last_affirmed': enc['last_affirmed'] 611 # } 612 # 613 # return not enc.same_payload(another_object = data) 614 #--------------------------------------------------------
615 - def __encounter_valid_for_save(self):
616 return True
617 #-------------------------------------------------------- 618 # event handling 619 #--------------------------------------------------------
620 - def __register_interests(self):
621 """Configure enabled event signals.""" 622 # synchronous signals 623 gmDispatcher.send(signal = 'register_pre_exit_callback', callback = self._pre_exit_callback) 624 625 # client internal signals 626 gmDispatcher.connect(signal = 'blobs.doc_med_mod_db', receiver = self._on_doc_mod_db) # visual progress notes 627 gmDispatcher.connect(signal = 'current_encounter_modified', receiver = self._on_current_encounter_modified) 628 gmDispatcher.connect(signal = 'current_encounter_switched', receiver = self._on_current_encounter_switched) 629 gmDispatcher.connect(signal = 'clin.rfe_code_mod_db', receiver = self._on_encounter_code_modified) 630 gmDispatcher.connect(signal = 'clin.aoe_code_mod_db', receiver = self._on_encounter_code_modified)
631 #--------------------------------------------------------
633 """Another patient is about to be activated. 634 635 Patient change will not proceed before this returns True. 636 """ 637 # don't worry about the encounter here - it will be offered 638 # for editing higher up if anything was saved to the EMR 639 if self.__pat is None: 640 return True 641 return self._NB_soap_editors.warn_on_unsaved_soap()
642 #--------------------------------------------------------
643 - def _pre_exit_callback(self):
644 """The client is about to (be) shut down. 645 646 Shutdown will not proceed before this returns. 647 """ 648 if self.__pat is None: 649 return True 650 651 # if self.__encounter_modified(): 652 # do_save_enc = gmGuiHelpers.gm_show_question ( 653 # aMessage = _( 654 # 'You have modified the details\n' 655 # 'of the current encounter.\n' 656 # '\n' 657 # 'Do you want to save those changes ?' 658 # ), 659 # aTitle = _('Starting new encounter') 660 # ) 661 # if do_save_enc: 662 # if not self.save_encounter(): 663 # gmDispatcher.send(signal = u'statustext', msg = _('Error saving current encounter.'), beep = True) 664 665 saved = self._NB_soap_editors.save_all_editors ( 666 emr = self.__pat.emr, 667 episode_name_candidates = [ 668 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), ''), 669 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), '') 670 ] 671 ) 672 if not saved: 673 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True) 674 return True
675 #--------------------------------------------------------
676 - def _on_doc_mod_db(self):
677 self.__refresh_current_editor()
678 #--------------------------------------------------------
680 self.__pat.emr.active_encounter.refetch_payload() 681 self.__refresh_encounter()
682 #--------------------------------------------------------
684 self.__refresh_encounter()
685 #--------------------------------------------------------
687 self.__refresh_encounter()
688 #-------------------------------------------------------- 689 # SOAP editor specific buttons 690 #--------------------------------------------------------
691 - def _on_discard_editor_button_pressed(self, event):
692 self._NB_soap_editors.close_current_editor() 693 event.Skip()
694 #--------------------------------------------------------
695 - def _on_new_editor_button_pressed(self, event):
696 self._NB_soap_editors.add_editor(allow_same_problem = True) 697 event.Skip()
698 #--------------------------------------------------------
699 - def _on_clear_editor_button_pressed(self, event):
700 self._NB_soap_editors.clear_current_editor() 701 event.Skip()
702 #--------------------------------------------------------
703 - def _on_save_note_button_pressed(self, event):
704 self._NB_soap_editors.save_current_editor ( 705 emr = self.__pat.emr, 706 episode_name_candidates = [ 707 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), ''), 708 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), '') 709 ] 710 ) 711 event.Skip()
712 #--------------------------------------------------------
713 - def _on_save_note_under_button_pressed(self, event):
714 encounter = gmEncounterWidgets.select_encounters ( 715 parent = self, 716 patient = self.__pat, 717 single_selection = True 718 ) 719 # cancelled or None selected: 720 if encounter is None: 721 return 722 723 self._NB_soap_editors.save_current_editor ( 724 emr = self.__pat.emr, 725 encounter = encounter['pk_encounter'], 726 episode_name_candidates = [ 727 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), ''), 728 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), '') 729 ] 730 ) 731 event.Skip()
732 #--------------------------------------------------------
733 - def _on_image_button_pressed(self, event):
734 self._NB_soap_editors.add_visual_progress_note_to_current_problem() 735 event.Skip()
736 #-------------------------------------------------------- 737 # encounter specific buttons 738 #--------------------------------------------------------
739 - def _on_save_encounter_button_pressed(self, event):
740 self.save_encounter() 741 event.Skip()
742 #-------------------------------------------------------- 743 # other buttons 744 #--------------------------------------------------------
745 - def _on_save_all_button_pressed(self, event):
746 self.save_encounter() 747 time.sleep(0.3) 748 event.Skip() 749 wx.SafeYield() 750 751 wx.CallAfter(self._save_all_button_pressed_bottom_half) 752 wx.SafeYield()
753 #--------------------------------------------------------
755 emr = self.__pat.emr 756 saved = self._NB_soap_editors.save_all_editors ( 757 emr = emr, 758 episode_name_candidates = [ 759 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), ''), 760 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), '') 761 ] 762 ) 763 if not saved: 764 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
765 766 #============================================================
767 -class cSoapNoteInputNotebook(wx.Notebook):
768 """A notebook holding panels with progress note editors. 769 770 There can be one or several progress note editor panels 771 for each episode being worked on. The editor class in 772 each panel is configurable. 773 774 There will always be one open editor. 775 """
776 - def __init__(self, *args, **kwargs):
777 778 kwargs['style'] = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER 779 780 wx.Notebook.__init__(self, *args, **kwargs) 781 782 _log.debug('created wx.Notebook: %s with ID %s', self.__class__.__name__, self.Id)
783 #-------------------------------------------------------- 784 # public API 785 #--------------------------------------------------------
786 - def add_editor(self, problem=None, allow_same_problem=False):
787 """Add a progress note editor page. 788 789 The way <allow_same_problem> is currently used in callers 790 it only applies to unassociated episodes. 791 """ 792 problem_to_add = problem 793 794 # determine label 795 if problem_to_add is None: 796 label = _('new problem') 797 else: 798 # normalize problem type 799 if isinstance(problem_to_add, gmEMRStructItems.cEpisode): 800 problem_to_add = gmEMRStructItems.episode2problem(episode = problem_to_add, allow_closed = True) 801 802 elif isinstance(problem_to_add, gmEMRStructItems.cHealthIssue): 803 problem_to_add = gmEMRStructItems.health_issue2problem(health_issue = problem_to_add, allow_irrelevant = True) 804 805 if not isinstance(problem_to_add, gmEMRStructItems.cProblem): 806 raise TypeError('cannot open progress note editor for [%s]' % problem_to_add) 807 808 label = problem_to_add['problem'] 809 # FIXME: configure maximum length 810 if len(label) > 23: 811 label = label[:21] + gmTools.u_ellipsis 812 813 # new unassociated problem or dupes allowed 814 if allow_same_problem: 815 new_page = gmProgressNotesEAWidgets.cProgressNotesEAPnl(self, -1, problem = problem_to_add) 816 result = self.AddPage ( 817 page = new_page, 818 text = label, 819 select = True 820 ) 821 return result 822 823 # real problem, no dupes allowed 824 # - raise existing editor 825 for page_idx in range(self.GetPageCount()): 826 page = self.GetPage(page_idx) 827 828 if problem_to_add is None: 829 if page.problem is None: 830 self.SetSelection(page_idx) 831 gmDispatcher.send(signal = 'statustext', msg = 'Raising existing editor.', beep = True) 832 return True 833 continue 834 835 # editor is for unassociated new problem 836 if page.problem is None: 837 continue 838 839 # editor is for episode 840 if page.problem['type'] == 'episode': 841 if page.problem['pk_episode'] == problem_to_add['pk_episode']: 842 self.SetSelection(page_idx) 843 gmDispatcher.send(signal = 'statustext', msg = 'Raising existing editor.', beep = True) 844 return True 845 continue 846 847 # editor is for health issue 848 if page.problem['type'] == 'issue': 849 if page.problem['pk_health_issue'] == problem_to_add['pk_health_issue']: 850 self.SetSelection(page_idx) 851 gmDispatcher.send(signal = 'statustext', msg = 'Raising existing editor.', beep = True) 852 return True 853 continue 854 855 # - or add new editor 856 new_page = gmProgressNotesEAWidgets.cProgressNotesEAPnl(parent = self, problem = problem_to_add) 857 result = self.AddPage ( 858 page = new_page, 859 text = label, 860 select = True 861 ) 862 863 return result
864 #--------------------------------------------------------
865 - def close_current_editor(self):
866 867 page_idx = self.GetSelection() 868 page = self.GetPage(page_idx) 869 870 if not page.empty: 871 really_discard = gmGuiHelpers.gm_show_question ( 872 _('Are you sure you really want to\n' 873 'discard this progress note ?\n' 874 ), 875 _('Discarding progress note') 876 ) 877 if really_discard is False: 878 return 879 880 self.DeletePage(page_idx) 881 882 # always keep one unassociated editor open 883 if self.GetPageCount() == 0: 884 self.add_editor()
885 #--------------------------------------------------------
886 - def save_current_editor(self, emr=None, episode_name_candidates=None, encounter=None):
887 page_idx = self.GetSelection() 888 _log.debug('saving editor on current page (#%s)', page_idx) 889 page = self.GetPage(page_idx) 890 if not page.save(emr = emr, episode_name_candidates = episode_name_candidates, encounter = encounter): 891 _log.debug('not saved, not deleting') 892 return False 893 894 _log.debug('deleting') 895 self.DeletePage(page_idx) 896 897 # always keep one unassociated editor open 898 if self.GetPageCount() == 0: 899 self.add_editor() 900 return True
901 #--------------------------------------------------------
902 - def warn_on_unsaved_soap(self):
903 for page_idx in range(self.GetPageCount()): 904 page = self.GetPage(page_idx) 905 if page.empty: 906 continue 907 908 gmGuiHelpers.gm_show_warning ( 909 _('There are unsaved progress notes !\n'), 910 _('Unsaved progress notes') 911 ) 912 return False 913 914 return True
915 #--------------------------------------------------------
916 - def save_all_editors(self, emr=None, episode_name_candidates=None):
917 918 _log.debug('saving editors: %s', self.GetPageCount()) 919 920 # always keep one unassociated editor open 921 if self.GetPageCount() == 0: 922 self.add_editor() 923 return True 924 925 # first of all save the current editor such 926 # as not to confuse the user by switching away 927 # from the page she invoked [save all] from 928 idx_of_current_page = self.GetSelection() 929 _log.debug('saving editor on current page (#%s)', idx_of_current_page) 930 all_closed = self.GetPage(idx_of_current_page).save(emr = emr, episode_name_candidates = episode_name_candidates) 931 if all_closed: 932 _log.debug('deleting') 933 self.DeletePage(idx_of_current_page) 934 idx_of_current_page = None 935 else: 936 _log.debug('not saved, not deleting') 937 938 # now save remaining editors from right to left 939 for page_idx in range((self.GetPageCount() - 1), -1, -1): 940 # skip current ? 941 if page_idx == idx_of_current_page: 942 # we tried and failed, no need to retry 943 continue 944 _log.debug('saving editor on page %s of %s', page_idx, self.GetPageCount()) 945 try: 946 self.ChangeSelection(page_idx) 947 _log.debug('editor raised') 948 except Exception: 949 _log.exception('cannot raise editor') 950 page = self.GetPage(page_idx) 951 if page.save(emr = emr, episode_name_candidates = episode_name_candidates): 952 _log.debug('saved, deleting') 953 self.DeletePage(page_idx) 954 else: 955 _log.debug('not saved, not deleting') 956 all_closed = False 957 958 # always keep one unassociated editor open 959 if self.GetPageCount() == 0: 960 self.add_editor() 961 962 return (all_closed is True)
963 #--------------------------------------------------------
964 - def clear_current_editor(self):
965 self.GetCurrentPage().clear()
966 #--------------------------------------------------------
967 - def get_current_problem(self):
968 return self.GetCurrentPage().problem
969 #--------------------------------------------------------
970 - def refresh_current_editor(self):
971 self.GetCurrentPage().refresh()
972 #--------------------------------------------------------
974 self.GetCurrentPage().add_visual_progress_note()
975 976 #============================================================ 977 #============================================================ 978 from Gnumed.wxGladeWidgets import wxgSimpleSoapPluginPnl 979
980 -class cSimpleSoapPluginPnl(wxgSimpleSoapPluginPnl.wxgSimpleSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
981 - def __init__(self, *args, **kwargs):
982 983 wxgSimpleSoapPluginPnl.wxgSimpleSoapPluginPnl.__init__(self, *args, **kwargs) 984 gmRegetMixin.cRegetOnPaintMixin.__init__(self) 985 986 self.__curr_pat = gmPerson.gmCurrentPatient() 987 self.__problem = None 988 self.__init_ui() 989 self.__register_interests()
990 #----------------------------------------------------- 991 # internal API 992 #-----------------------------------------------------
993 - def __init_ui(self):
994 self._LCTRL_problems.set_columns(columns = [_('Problem list')]) 995 self._LCTRL_problems.activate_callback = self._on_problem_activated 996 self._LCTRL_problems.item_tooltip_callback = self._on_get_problem_tooltip 997 998 self._splitter_main.SetSashGravity(0.5) 999 splitter_width = self._splitter_main.GetSize()[0] 1000 self._splitter_main.SetSashPosition(splitter_width // 2, True) 1001 1002 self._TCTRL_soap.Disable() 1003 self._BTN_save_soap.Disable() 1004 self._BTN_clear_soap.Disable()
1005 #-----------------------------------------------------
1006 - def __reset_ui(self):
1007 self._LCTRL_problems.set_string_items() 1008 self._TCTRL_soap_problem.SetValue(_('<above, double-click problem to start entering SOAP note>')) 1009 self._TCTRL_soap.SetValue('') 1010 self._CHBOX_filter_by_problem.SetLabel(_('&Filter by problem')) 1011 self._TCTRL_journal.SetValue('') 1012 1013 self._TCTRL_soap.Disable() 1014 self._BTN_save_soap.Disable() 1015 self._BTN_clear_soap.Disable()
1016 #-----------------------------------------------------
1017 - def __save_soap(self):
1018 if not self.__curr_pat.connected: 1019 return None 1020 1021 if self.__problem is None: 1022 return None 1023 1024 saved = self.__curr_pat.emr.add_clin_narrative ( 1025 note = self._TCTRL_soap.GetValue().strip(), 1026 soap_cat = 'u', 1027 episode = self.__problem 1028 ) 1029 1030 if saved is None: 1031 return False 1032 1033 self._TCTRL_soap.SetValue('') 1034 self.__refresh_journal() 1035 return True
1036 #-----------------------------------------------------
1037 - def __perhaps_save_soap(self):
1038 if self._TCTRL_soap.GetValue().strip() == '': 1039 return True 1040 if self.__problem is None: 1041 # FIXME: this could potentially lose input 1042 self._TCTRL_soap.SetValue('') 1043 return None 1044 save_it = gmGuiHelpers.gm_show_question ( 1045 title = _('Saving SOAP note'), 1046 question = _('Do you want to save the SOAP note ?') 1047 ) 1048 if save_it: 1049 return self.__save_soap() 1050 return False
1051 #-----------------------------------------------------
1052 - def __refresh_problem_list(self):
1053 self._LCTRL_problems.set_string_items() 1054 emr = self.__curr_pat.emr 1055 epis = emr.get_episodes(open_status = True) 1056 if len(epis) > 0: 1057 self._LCTRL_problems.set_string_items(items = [ '%s%s' % ( 1058 e['description'], 1059 gmTools.coalesce(e['health_issue'], '', ' (%s)') 1060 ) for e in epis ]) 1061 self._LCTRL_problems.set_data(epis)
1062 #-----------------------------------------------------
1063 - def __refresh_journal(self):
1064 self._TCTRL_journal.SetValue('') 1065 epi = self._LCTRL_problems.get_selected_item_data(only_one = True) 1066 1067 if epi is not None: 1068 self._CHBOX_filter_by_problem.SetLabel(_('&Filter by problem %s%s%s') % ( 1069 gmTools.u_left_double_angle_quote, 1070 epi['description'], 1071 gmTools.u_right_double_angle_quote 1072 )) 1073 self._CHBOX_filter_by_problem.Refresh() 1074 1075 if not self._CHBOX_filter_by_problem.IsChecked(): 1076 self._TCTRL_journal.SetValue(self.__curr_pat.emr.format_summary()) 1077 return 1078 1079 if epi is None: 1080 return 1081 1082 self._TCTRL_journal.SetValue(epi.format_as_journal())
1083 #----------------------------------------------------- 1084 # event handling 1085 #-----------------------------------------------------
1086 - def __register_interests(self):
1087 """Configure enabled event signals.""" 1088 # client internal signals 1089 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection) 1090 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection) 1091 gmDispatcher.connect(signal = 'clin.episode_mod_db', receiver = self._on_episode_issue_mod_db) 1092 gmDispatcher.connect(signal = 'clin.health_issue_mod_db', receiver = self._on_episode_issue_mod_db) 1093 1094 # synchronous signals 1095 self.__curr_pat.register_before_switching_from_patient_callback(callback = self._before_switching_from_patient_callback) 1096 gmDispatcher.send(signal = 'register_pre_exit_callback', callback = self._pre_exit_callback)
1097 #-----------------------------------------------------
1099 """Another patient is about to be activated. 1100 1101 Patient change will not proceed before this returns True. 1102 """ 1103 if not self.__curr_pat.connected: 1104 return True 1105 self.__perhaps_save_soap() 1106 self.__problem = None 1107 return True
1108 #-----------------------------------------------------
1109 - def _pre_exit_callback(self):
1110 """The client is about to be shut down. 1111 1112 Shutdown will not proceed before this returns. 1113 """ 1114 if not self.__curr_pat.connected: 1115 return 1116 if not self.__save_soap(): 1117 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save SimpleNotes SOAP note.'), beep = True) 1118 return
1119 #-----------------------------------------------------
1121 self.__reset_ui()
1122 #-----------------------------------------------------
1123 - def _on_post_patient_selection(self):
1124 self._schedule_data_reget()
1125 #-----------------------------------------------------
1126 - def _on_episode_issue_mod_db(self):
1127 self._schedule_data_reget()
1128 #-----------------------------------------------------
1129 - def _on_problem_activated(self, event):
1130 self.__perhaps_save_soap() 1131 epi = self._LCTRL_problems.get_selected_item_data(only_one = True) 1132 self._TCTRL_soap_problem.SetValue(_('Progress note: %s%s') % ( 1133 epi['description'], 1134 gmTools.coalesce(epi['health_issue'], '', ' (%s)') 1135 )) 1136 self.__problem = epi 1137 self._TCTRL_soap.SetValue('') 1138 1139 self._TCTRL_soap.Enable() 1140 self._BTN_save_soap.Enable() 1141 self._BTN_clear_soap.Enable()
1142 #-----------------------------------------------------
1143 - def _on_get_problem_tooltip(self, episode):
1144 return episode.format ( 1145 patient = self.__curr_pat, 1146 with_summary = False, 1147 with_codes = True, 1148 with_encounters = False, 1149 with_documents = False, 1150 with_hospital_stays = False, 1151 with_procedures = False, 1152 with_family_history = False, 1153 with_tests = False, 1154 with_vaccinations = False, 1155 with_health_issue = True 1156 )
1157 #-----------------------------------------------------
1158 - def _on_list_item_selected(self, event):
1159 event.Skip() 1160 self.__refresh_journal()
1161 #-----------------------------------------------------
1162 - def _on_filter_by_problem_checked(self, event):
1163 event.Skip() 1164 self.__refresh_journal()
1165 #-----------------------------------------------------
1166 - def _on_add_problem_button_pressed(self, event):
1167 event.Skip() 1168 epi_name = wx.GetTextFromUser ( 1169 _('Please enter a name for the new problem:'), 1170 caption = _('Adding a problem'), 1171 parent = self 1172 ).strip() 1173 if epi_name == '': 1174 return 1175 self.__curr_pat.emr.add_episode ( 1176 episode_name = epi_name, 1177 pk_health_issue = None, 1178 is_open = True 1179 )
1180 #-----------------------------------------------------
1181 - def _on_edit_problem_button_pressed(self, event):
1182 event.Skip() 1183 epi = self._LCTRL_problems.get_selected_item_data(only_one = True) 1184 if epi is None: 1185 return 1186 gmEMRStructWidgets.edit_episode(parent = self, episode = epi)
1187 #-----------------------------------------------------
1188 - def _on_delete_problem_button_pressed(self, event):
1189 event.Skip() 1190 epi = self._LCTRL_problems.get_selected_item_data(only_one = True) 1191 if epi is None: 1192 return 1193 if not gmEMRStructItems.delete_episode(episode = epi): 1194 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete problem. There is still clinical data recorded for it.'))
1195 #-----------------------------------------------------
1196 - def _on_save_soap_button_pressed(self, event):
1197 event.Skip() 1198 self.__save_soap()
1199 #-----------------------------------------------------
1200 - def _on_clear_soap_button_pressed(self, event):
1201 event.Skip() 1202 self._TCTRL_soap.SetValue('')
1203 #----------------------------------------------------- 1204 # reget-on-paint mixin API 1205 #-----------------------------------------------------
1206 - def _populate_with_data(self):
1207 self.__refresh_problem_list() 1208 self.__refresh_journal() 1209 self._TCTRL_soap.SetValue('') 1210 return True
1211 1212 #============================================================ 1213 # main 1214 #------------------------------------------------------------ 1215 if __name__ == '__main__': 1216 1217 if len(sys.argv) < 2: 1218 sys.exit() 1219 1220 if sys.argv[1] != 'test': 1221 sys.exit() 1222 1223 gmI18N.activate_locale() 1224 gmI18N.install_domain(domain = 'gnumed') 1225 1226 #----------------------------------------
1227 - def test_cSoapPluginPnl():
1228 patient = gmPersonSearch.ask_for_patient() 1229 if patient is None: 1230 print("No patient. Exiting gracefully...") 1231 return 1232 set_active_patient(patient=patient) 1233 1234 application = wx.PyWidgetTester(size=(800,500)) 1235 soap_input = cSoapPluginPnl(application.frame, -1) 1236 application.frame.Show(True) 1237 soap_input._schedule_data_reget() 1238 application.MainLoop()
1239 #---------------------------------------- 1240 #test_cSoapPluginPnl() 1241