Home | Trees | Indices | Help |
|
---|
|
1 """GNUmed SOAP related widgets. 2 """ 3 #============================================================ 4 __author__ = "Carlos Moro <cfmoro1976@yahoo.es>, K.Hilbert <Karsten.Hilbert@gmx.net>" 5 __license__ = "GPL" 6 7 # std library 8 import logging 9 10 11 # 3rd party 12 import wx 13 14 15 # GNUmed 16 from Gnumed.pycommon import gmDispatcher, gmI18N, gmExceptions, gmMatchProvider, gmTools, gmCfg 17 from Gnumed.wxpython import gmResizingWidgets, gmPhraseWheel, gmEMRStructWidgets, gmGuiHelpers, gmRegetMixin, gmEditArea, gmPatSearchWidgets 18 from Gnumed.business import gmPerson, gmEMRStructItems, gmSOAPimporter, gmPraxis, gmPersonSearch, gmStaff 19 20 _log = logging.getLogger('gm.ui') 21 22 #============================================================24 ea = gmEMRStructWidgets.cHealthIssueEditArea ( 25 parent, 26 -1, 27 wx.DefaultPosition, 28 wx.DefaultSize, 29 wx.NO_BORDER | wx.TAB_TRAVERSAL, 30 data_sink = data_sink 31 ) 32 popup = gmEditArea.cEditAreaPopup ( 33 parent = parent, 34 id = -1, 35 title = '', 36 pos = pos, 37 size = size, 38 style = style, 39 name = '', 40 edit_area = ea 41 ) 42 return popup43 #============================================================45 ea = gmVaccWidgets.cVaccinationEditArea ( 46 parent = parent, 47 id = -1, 48 pos = pos, 49 size = size, 50 style = style, 51 data_sink = data_sink 52 ) 53 popup = gmEditArea.cEditAreaPopup ( 54 parent = parent, 55 id = -1, 56 title = _('Enter vaccination given'), 57 pos = pos, 58 size = size, 59 style = style, 60 name = '', 61 edit_area = ea 62 ) 63 return popup64 #============================================================ 65 # FIXME: keywords hardcoded for now, load from cfg in backend instead 66 progress_note_keywords = { 67 's': { 68 '$missing_action': {}, 69 'phx$': { 70 'widget_factory': create_issue_popup, 71 'widget_data_sink': None 72 }, 73 'ea$:': { 74 'widget_factory': create_issue_popup, 75 'widget_data_sink': None 76 }, 77 '$vacc': { 78 'widget_factory': create_vacc_popup, 79 'widget_data_sink': None 80 }, 81 'impf:': { 82 'widget_factory': create_vacc_popup, 83 'widget_data_sink': None 84 }, 85 'icpc:': {}, 86 'icpc?': {} 87 }, 88 'o': { 89 'icpc:': {}, 90 'icpc?': {} 91 }, 92 'a': { 93 'icpc:': {}, 94 'icpc?': {} 95 }, 96 'p': { 97 '$vacc': { 98 'widget_factory': create_vacc_popup, 99 'widget_data_sink': None 100 }, 101 'icpc:': {}, 102 'icpc?': {} 103 } 104 } 105 #============================================================107 """A notebook holding panels with progress note editors. 108 109 There is one progress note editor panel for each episode being worked on. 110 """340 #-------------------------------------------------------- 341 # def _on_application_closing(self): 342 # """GNUmed is shutting down.""" 343 # print "[%s]: the application is closing down" % self.__class__.__name__ 344 # print "************************************" 345 # print "need to ask user about SOAP saving !" 346 # print "************************************" 347 #-------------------------------------------------------- 348 # def _on_episodes_modified(self): 349 # print "[%s]: episode modified" % self.__class__.__name__ 350 # print "need code to deal with:" 351 # print "- deleted episode that we show so we can notify the user" 352 # print "- renamed episode so we can update our episode label" 353 # self._schedule_data_reget() 354 # pass 355 #============================================================112 wx.Notebook.__init__ ( 113 self, 114 parent = parent, 115 id = id, 116 pos = pos, 117 size = size, 118 style = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER, 119 name = self.__class__.__name__ 120 ) 121 _log.debug('created wx.Notebook: %s with ID %s', self.__class__.__name__, self.Id) 122 gmRegetMixin.cRegetOnPaintMixin.__init__(self) 123 self.__pat = gmPerson.gmCurrentPatient() 124 self.__do_layout() 125 self.__register_interests()126 #-------------------------------------------------------- 127 # public API 128 #--------------------------------------------------------130 """Add a progress note editor page. 131 132 The way <allow_same_problem> is currently used in callers 133 it only applies to unassociated episodes. 134 """ 135 problem_to_add = problem 136 137 # determine label 138 if problem_to_add is None: 139 label = _('new problem') 140 else: 141 # normalize problem type 142 emr = self.__pat.emr 143 if isinstance(problem_to_add, gmEMRStructItems.cEpisode): 144 problem_to_add = emr.episode2problem(episode = problem_to_add) 145 elif isinstance(problem_to_add, gmEMRStructItems.cHealthIssue): 146 problem_to_add = emr.health_issue2problem(issue = problem_to_add) 147 if not isinstance(problem_to_add, gmEMRStructItems.cProblem): 148 raise TypeError('cannot open progress note editor for [%s]' % problem_to_add) 149 label = problem_to_add['problem'] 150 # FIXME: configure maximum length 151 if len(label) > 23: 152 label = label[:21] + gmTools.u_ellipsis 153 154 if allow_same_problem: 155 new_page = cResizingSoapPanel(parent = self, problem = problem_to_add) 156 result = self.AddPage ( 157 page = new_page, 158 text = label, 159 select = True 160 ) 161 return result 162 163 # check for dupes 164 # new unassociated problem 165 if problem_to_add is None: 166 # check for dupes 167 for page_idx in range(self.GetPageCount()): 168 page = self.GetPage(page_idx) 169 # found 170 if page.get_problem() is None: 171 self.SetSelection(page_idx) 172 return True 173 continue 174 # not found 175 new_page = cResizingSoapPanel(parent = self, problem = problem_to_add) 176 result = self.AddPage ( 177 page = new_page, 178 text = label, 179 select = True 180 ) 181 return result 182 183 # real problem 184 # - raise existing editor ? 185 for page_idx in range(self.GetPageCount()): 186 page = self.GetPage(page_idx) 187 problem_of_page = page.get_problem() 188 # editor is for unassociated new problem 189 if problem_of_page is None: 190 continue 191 # editor is for episode 192 if problem_of_page['type'] == 'episode': 193 if problem_to_add['type'] == 'issue': 194 is_equal = (problem_of_page['pk_health_issue'] == problem_to_add['pk_health_issue']) 195 else: 196 is_equal = (problem_of_page['pk_episode'] == problem_to_add['pk_episode']) 197 if is_equal: 198 self.SetSelection(page_idx) 199 return True 200 continue 201 # editor is for health issue 202 if problem_of_page['type'] == 'issue': 203 if problem_of_page['pk_health_issue'] == problem_to_add['pk_health_issue']: 204 self.SetSelection(page_idx) 205 return True 206 continue 207 208 # - add new editor 209 new_page = cResizingSoapPanel(parent = self, problem = problem_to_add) 210 result = self.AddPage ( 211 page = new_page, 212 text = label, 213 select = True 214 ) 215 216 return result217 #--------------------------------------------------------219 220 page_idx = self.GetSelection() 221 page = self.GetPage(page_idx) 222 223 if not page.editor_empty(): 224 really_discard = gmGuiHelpers.gm_show_question ( 225 _('Are you sure you really want to\n' 226 'discard this progress note ?\n' 227 ), 228 _('Discarding progress note') 229 ) 230 if really_discard is False: 231 return 232 233 self.DeletePage(page_idx) 234 235 # always keep one unassociated editor open 236 if self.GetPageCount() == 0: 237 self.add_editor()238 #--------------------------------------------------------240 241 for page_idx in range(self.GetPageCount()): 242 page = self.GetPage(page_idx) 243 if page.editor_empty(): 244 continue 245 246 gmGuiHelpers.gm_show_warning ( 247 _('There are unsaved progress notes !\n'), 248 _('Unsaved progress notes') 249 ) 250 return False 251 252 return True253 #--------------------------------------------------------255 save_all = False 256 dlg = None 257 for page_idx in range(self.GetPageCount()): 258 page = self.GetPage(page_idx) 259 if page.editor_empty(): 260 continue 261 262 if dlg is None: 263 dlg = gmGuiHelpers.c3ButtonQuestionDlg ( 264 self, 265 -1, 266 caption = _('Unsaved progress note'), 267 question = _( 268 'This progress note has not been saved yet.\n' 269 '\n' 270 'Do you want to save it or discard it ?\n\n' 271 ), 272 button_defs = [ 273 {'label': _('&Save'), 'tooltip': _('Save this progress note'), 'default': True}, 274 {'label': _('&Discard'), 'tooltip': _('Discard this progress note'), 'default': False}, 275 {'label': _('Save &all'), 'tooltip': _('Save all remaining unsaved progress notes'), 'default': False} 276 ] 277 ) 278 279 if not save_all: 280 self.ChangeSelection(page_idx) 281 decision = dlg.ShowModal() 282 if decision == wx.ID_NO: 283 _log.info('user requested discarding of unsaved progress note') 284 continue 285 if decision == wx.ID_CANCEL: 286 save_all = True 287 page.save() 288 289 if dlg is not None: 290 dlg.Destroy()291 #-------------------------------------------------------- 292 # internal API 293 #--------------------------------------------------------295 # add one empty unassociated progress note editor - which to 296 # have (by all sensible accounts) seems to be the intent when 297 # instantiating this class 298 self.add_editor()299 #-------------------------------------------------------- 300 # reget mixin API 301 #--------------------------------------------------------303 print('[%s._populate_with_data] nothing to do, really...' % self.__class__.__name__) 304 return True305 #-------------------------------------------------------- 306 # event handling 307 #--------------------------------------------------------309 """Configure enabled event signals 310 """ 311 # wxPython events 312 313 # client internal signals 314 gmDispatcher.connect(signal = 'post_patient_selection', receiver=self._on_post_patient_selection) 315 # gmDispatcher.connect(signal = u'application_closing', receiver=self._on_application_closing) 316 317 self.__pat.register_before_switching_from_patient_callback(callback = self._before_switching_from_patient_callback) 318 319 gmDispatcher.send(signal = 'register_pre_exit_callback', callback = self._pre_exit_callback)320 #--------------------------------------------------------322 """Another patient is about to be activated. 323 324 Patient change will not proceed before this returns True. 325 """ 326 return self.warn_on_unsaved_soap()327 #--------------------------------------------------------329 """The client is about to be shut down. 330 331 Shutdown will not proceed before this returns. 332 """ 333 self.save_unsaved_soap()334 #--------------------------------------------------------336 """Patient changed.""" 337 self.DeleteAllPages() 338 self.add_editor() 339 self._schedule_data_reget()357 """A panel for entering multiple progress notes in context. 358 359 Expects to be used as a notebook page. 360 361 Left hand side: 362 - problem list (health issues and active episodes) 363 364 Right hand side: 365 - notebook with progress note editors 366 367 Listens to patient change signals, thus acts on the current patient. 368 """ 369 #--------------------------------------------------------628 #============================================================ 636 #============================================================ 637 # FIXME: this should be a more generic(ally named) class 638 # FIXME: living elsewhere371 """Contructs a new instance of SOAP input panel 372 373 @param parent: Wx parent widget 374 @param id: Wx widget id 375 """ 376 # Call parents constructors 377 wx.Panel.__init__ ( 378 self, 379 parent = parent, 380 id = id, 381 pos = wx.DefaultPosition, 382 size = wx.DefaultSize, 383 style = wx.NO_BORDER 384 ) 385 self.__pat = gmPerson.gmCurrentPatient() 386 387 # ui contruction and event handling set up 388 self.__do_layout() 389 self.__register_interests() 390 self.reset_ui_content()391 #-------------------------------------------------------- 392 # public API 393 #--------------------------------------------------------395 """ 396 Clear all information from input panel 397 """ 398 self.__LST_problems.Clear() 399 self.__soap_notebook.DeleteAllPages() 400 self.__soap_notebook.add_editor()401 #-------------------------------------------------------- 402 # internal helpers 403 #--------------------------------------------------------405 """Arrange widgets. 406 407 left: problem list (mix of issues and episodes) 408 right: soap editors 409 """ 410 # SOAP input panel main splitter window 411 self.__splitter = wx.SplitterWindow(self, -1) 412 413 # left hand side 414 PNL_list = wx.Panel(self.__splitter, -1) 415 # - header 416 list_header = wx.StaticText ( 417 parent = PNL_list, 418 id = -1, 419 label = _('Active problems'), 420 style = wx.NO_BORDER | wx.ALIGN_CENTRE 421 ) 422 # - problem list 423 self.__LST_problems = wx.ListBox ( 424 PNL_list, 425 -1, 426 style= wx.NO_BORDER 427 ) 428 # - arrange 429 szr_left = wx.BoxSizer(wx.VERTICAL) 430 szr_left.Add(list_header, 0) 431 szr_left.Add(self.__LST_problems, 1, wx.EXPAND) 432 PNL_list.SetSizerAndFit(szr_left) 433 434 # right hand side 435 # - soap inputs panel 436 PNL_soap_editors = wx.Panel(self.__splitter, -1) 437 # - progress note notebook 438 self.__soap_notebook = cProgressNoteInputNotebook(PNL_soap_editors, -1) 439 # - buttons 440 self.__BTN_add_unassociated = wx.Button(PNL_soap_editors, -1, _('&New')) 441 tt = _( 442 'Add editor for a new unassociated progress note.\n\n' 443 'There is a configuration option whether or not to\n' 444 'allow several new unassociated progress notes at once.' 445 ) 446 self.__BTN_add_unassociated.SetToolTip(tt) 447 448 self.__BTN_save = wx.Button(PNL_soap_editors, -1, _('&Save')) 449 self.__BTN_save.SetToolTip(_('Save progress note into medical record and close this editor.')) 450 451 self.__BTN_clear = wx.Button(PNL_soap_editors, -1, _('&Clear')) 452 self.__BTN_clear.SetToolTip(_('Clear this progress note editor.')) 453 454 self.__BTN_discard = wx.Button(PNL_soap_editors, -1, _('&Discard')) 455 self.__BTN_discard.SetToolTip(_('Discard progress note and close this editor. You will loose any data already typed into this editor !')) 456 457 # - arrange 458 szr_btns_right = wx.BoxSizer(wx.HORIZONTAL) 459 szr_btns_right.Add(self.__BTN_add_unassociated, 0, wx.SHAPED) 460 szr_btns_right.Add(self.__BTN_clear, 0, wx.SHAPED) 461 szr_btns_right.Add(self.__BTN_save, 0, wx.SHAPED) 462 szr_btns_right.Add(self.__BTN_discard, 0, wx.SHAPED) 463 464 szr_right = wx.BoxSizer(wx.VERTICAL) 465 szr_right.Add(self.__soap_notebook, 1, wx.EXPAND) 466 szr_right.Add(szr_btns_right) 467 PNL_soap_editors.SetSizerAndFit(szr_right) 468 469 # arrange widgets 470 self.__splitter.SetMinimumPaneSize(20) 471 self.__splitter.SplitVertically(PNL_list, PNL_soap_editors) 472 473 szr_main = wx.BoxSizer(wx.VERTICAL) 474 szr_main.Add(self.__splitter, 1, wx.EXPAND, 0) 475 self.SetSizerAndFit(szr_main)476 #--------------------------------------------------------478 """Update health problems list. 479 """ 480 self.__LST_problems.Clear() 481 emr = self.__pat.emr 482 problems = emr.get_problems() 483 for problem in problems: 484 if not problem['problem_active']: 485 continue 486 if problem['type'] == 'issue': 487 issue = emr.problem2issue(problem) 488 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue']) 489 if last_encounter is None: 490 last = issue['modified_when'].strftime('%m/%Y') 491 else: 492 last = last_encounter['last_affirmed'].strftime('%m/%Y') 493 label = '%s: %s "%s"' % (last, problem['l10n_type'], problem['problem']) 494 elif problem['type'] == 'episode': 495 epi = emr.problem2episode(problem) 496 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode']) 497 if last_encounter is None: 498 last = epi['episode_modified_when'].strftime('%m/%Y') 499 else: 500 last = last_encounter['last_affirmed'].strftime('%m/%Y') 501 label = '%s: %s "%s"%s' % ( 502 last, 503 problem['l10n_type'], 504 problem['problem'], 505 gmTools.coalesce(initial = epi['health_issue'], instead = '', template_initial = ' (%s)') 506 ) 507 self.__LST_problems.Append(label, problem) 508 splitter_width = self.__splitter.GetSize()[0] 509 self.__splitter.SetSashPosition((splitter_width / 2), True) 510 self.Refresh() 511 #self.Update() 512 return True513 #-------------------------------------------------------- 514 # event handling 515 #--------------------------------------------------------517 """Configure enabled event signals 518 """ 519 # wxPython events 520 self.__LST_problems.Bind(wx.EVT_LISTBOX_DCLICK, self.__on_problem_activated) 521 self.__BTN_save.Bind(wx.EVT_BUTTON, self.__on_save) 522 self.__BTN_clear.Bind(wx.EVT_BUTTON, self.__on_clear) 523 self.__BTN_discard.Bind(wx.EVT_BUTTON, self.__on_discard) 524 self.__BTN_add_unassociated.Bind(wx.EVT_BUTTON, self.__on_add_unassociated) 525 #wx.EVT_LISTBOX_DCLICK(self.__LST_problems, self.__LST_problems.GetId(), self.__on_problem_activated) 526 #wx.EVT_BUTTON(self.__BTN_save, self.__BTN_save.GetId(), self.__on_save) 527 #wx.EVT_BUTTON(self.__BTN_clear, self.__BTN_clear.GetId(), self.__on_clear) 528 #wx.EVT_BUTTON(self.__BTN_discard, self.__BTN_discard.GetId(), self.__on_discard) 529 #wx.EVT_BUTTON(self.__BTN_add_unassociated, self.__BTN_add_unassociated.GetId(), self.__on_add_unassociated) 530 531 # client internal signals 532 gmDispatcher.connect(signal='post_patient_selection', receiver=self._on_post_patient_selection) 533 gmDispatcher.connect(signal = 'clin.episode_mod_db', receiver = self._on_episode_issue_mod_db) 534 gmDispatcher.connect(signal = 'clin.health_issue_mod_db', receiver = self._on_episode_issue_mod_db)535 #--------------------------------------------------------537 """Patient changed.""" 538 if self.GetParent().GetCurrentPage() == self: 539 self.reset_ui_content()540 #-------------------------------------------------------- 544 #--------------------------------------------------------546 """Clear raised SOAP input widget. 547 """ 548 soap_nb_page = self.__soap_notebook.GetPage(self.__soap_notebook.GetSelection()) 549 soap_nb_page.Clear()550 #--------------------------------------------------------552 """Discard raised SOAP input widget. 553 554 Will throw away data ! 555 """ 556 self.__soap_notebook.close_current_editor()557 #--------------------------------------------------------559 """Add new editor for as-yet unassociated progress note. 560 561 Clinical logic as per discussion with Jim Busser: 562 563 - if patient has no episodes: 564 - new patient 565 - always allow several NEWs 566 - if patient has episodes: 567 - allow several NEWs per configuration 568 """ 569 emr = self.__pat.emr 570 epis = emr.get_episodes() 571 572 if len(epis) == 0: 573 value = True 574 else: 575 dbcfg = gmCfg.cCfgSQL() 576 value = bool(dbcfg.get2 ( 577 option = 'horstspace.soap_editor.allow_same_episode_multiple_times', 578 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace, 579 bias = 'user', 580 default = False 581 )) 582 583 self.__soap_notebook.add_editor(allow_same_problem = value)584 #--------------------------------------------------------586 """ 587 When the user changes health issue selection, update selected issue 588 reference and update buttons according its input status. 589 590 when the user selects a problem in the problem list: 591 - check whether selection is issue or episode 592 - if editor for episode exists: focus it 593 - if no editor for episode exists: create one and focus it 594 """ 595 problem_idx = self.__LST_problems.GetSelection() 596 problem = self.__LST_problems.GetClientData(problem_idx) 597 598 if self.__soap_notebook.add_editor(problem = problem): 599 return True 600 601 gmGuiHelpers.gm_show_error ( 602 aMessage = _( 603 'Cannot open progress note editor for\n\n' 604 '[%s].\n\n' 605 ) % problem['problem'], 606 aTitle = _('opening progress note editor') 607 ) 608 return False609 #--------------------------------------------------------611 """Save data to backend and close editor. 612 """ 613 page_idx = self.__soap_notebook.GetSelection() 614 soap_nb_page = self.__soap_notebook.GetPage(page_idx) 615 if not soap_nb_page.save(): 616 gmDispatcher.send(signal='statustext', msg=_('Problem saving progress note: duplicate information ?')) 617 return False 618 self.__soap_notebook.DeletePage(page_idx) 619 # always keep one unassociated editor open 620 self.__soap_notebook.add_editor() 621 #self.__refresh_problem_list() 622 return True623 #-------------------------------------------------------- 624 # notebook plugin API 625 #--------------------------------------------------------640 641 _data_savers = {} 642 645 #--------------------------------------------------------683 #-------------------------------------------------------- 684 # def remove_data(self, popup_type=None, desc=None): 685 # del self.__data[popup_type][desc] 686 #-------------------------------------------------------- 687 # def get_descs(self, popup_type=None, origination_soap=None): 688 # def get_data(self, desc=None): 689 # def rename_data(self, old_desc=None, new_desc=None): 690 #============================================================647 # FIXME: do fancy validations 648 649 print("storing popup data:", desc) 650 print("type", popup_type) 651 print("data", data) 652 653 # verify structure 654 try: 655 self.__data[popup_type] 656 except KeyError: 657 self.__data[popup_type] = {} 658 # store new data 659 self.__data[popup_type][desc] = { 660 'data': data 661 } 662 # remove old data if necessary 663 try: 664 del self.__data[popup_type][old_desc] 665 except: 666 pass 667 return True668 #--------------------------------------------------------670 for popup_type in self.__data.keys(): 671 try: 672 saver_func = self.__data_savers[popup_type] 673 except KeyError: 674 _log.exception('no saver for popup data type [%s] configured', popup_type) 675 return False 676 for desc in self.__data[popup_type].keys(): 677 data = self.__data[popup_type][desc]['data'] 678 saver_func(data) 679 return True680 #--------------------------------------------------------692862 #============================================================694 """Resizing SOAP note input editor. 695 696 This is a wrapper around a few resizing STCs (the 697 labels and categories are settable) which are 698 customized to accept progress note input. It provides 699 the unified resizing behaviour. 700 701 Knows how to save it's data into the backend. 702 703 @param input_defs: note's labels and categories 704 @type input_defs: list of cSOAPLineDef instances 705 """ 706 if input_defs is None or len(input_defs) == 0: 707 raise gmExceptions.ConstructorError('cannot generate note with field defs [%s]' % input_defs) 708 709 # FIXME: *actually* this should be a session-local 710 # FIXME: holding store at the c_ClinicalRecord level 711 self.__embedded_data_holder = cPopupDataHolder() 712 713 self.__input_defs = input_defs 714 715 gmResizingWidgets.cResizingWindow.__init__(self, parent, id=-1, size=size) 716 717 self.__problem = problem 718 if isinstance(problem, gmEMRStructItems.cEpisode): 719 self.__problem = emr.episode2problem(episode = problem) 720 elif isinstance(problem, gmEMRStructItems.cHealthIssue): 721 self.__problem = emr.health_issue2problem(issue = problem) 722 self.__pat = gmPerson.gmCurrentPatient()723 #-------------------------------------------------------- 724 # cResizingWindow API 725 #--------------------------------------------------------727 """Visually display input note according to user defined labels. 728 """ 729 # configure keywords 730 for soap_cat in progress_note_keywords.keys(): 731 category = progress_note_keywords[soap_cat] 732 for kwd in category.keys(): 733 category[kwd]['widget_data_sink'] = self.__embedded_data_holder.store_data 734 input_fields = [] 735 # add fields to edit widget 736 # note: this may produce identically labelled lines 737 for line_def in self.__input_defs: 738 input_field = gmResizingWidgets.cResizingSTC(self, -1, data = line_def) 739 input_field.SetText(line_def.text) 740 kwds = progress_note_keywords[line_def.soap_cat] 741 input_field.set_keywords(popup_keywords=kwds) 742 # FIXME: pending matcher setup 743 self.AddWidget(widget=input_field, label=line_def.label) 744 self.Newline() 745 input_fields.append(input_field) 746 # setup tab navigation between input fields 747 for field_idx in range(len(input_fields)): 748 # previous 749 try: 750 input_fields[field_idx].prev_in_tab_order = input_fields[field_idx-1] 751 except IndexError: 752 input_fields[field_idx].prev_in_tab_order = None 753 # next 754 try: 755 input_fields[field_idx].next_in_tab_order = input_fields[field_idx+1] 756 except IndexError: 757 input_fields[field_idx].next_in_tab_order = None758 #-------------------------------------------------------- 759 # public API 760 #--------------------------------------------------------762 """Save data into backend.""" 763 764 # fill progress_note for import 765 progress_note = [] 766 aoe = '' 767 rfe = '' 768 has_rfe = False 769 soap_lines_contents = self.GetValue() 770 for line_content in soap_lines_contents.values(): 771 if line_content.text.strip() == '': 772 continue 773 progress_note.append ({ 774 gmSOAPimporter.soap_bundle_SOAP_CAT_KEY: line_content.data.soap_cat, 775 gmSOAPimporter.soap_bundle_TYPES_KEY: [], # these types need to come from the editor 776 gmSOAPimporter.soap_bundle_TEXT_KEY: line_content.text.rstrip() 777 }) 778 if line_content.data.is_rfe: 779 has_rfe = True 780 rfe += line_content.text.rstrip() 781 if line_content.data.soap_cat == 'a': 782 aoe += line_content.text.rstrip() 783 784 emr = self.__pat.emr 785 786 # - new episode, must get name from narrative (or user) 787 if (self.__problem is None) or (self.__problem['type'] == 'issue'): 788 # work out episode name 789 epi_name = '' 790 if len(aoe) != 0: 791 epi_name = aoe 792 else: 793 epi_name = rfe 794 795 dlg = wx.TextEntryDialog ( 796 parent = self, 797 message = _('Enter a descriptive name for this new problem:'), 798 caption = _('Creating a problem (episode) to save the notelet under ...'), 799 defaultValue = epi_name.replace('\r', '//').replace('\n', '//'), 800 style = wx.OK | wx.CANCEL | wx.CENTRE 801 ) 802 decision = dlg.ShowModal() 803 if decision != wx.ID_OK: 804 return False 805 806 epi_name = dlg.GetValue().strip() 807 if epi_name == '': 808 gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note')) 809 return False 810 811 # new unassociated episode 812 new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True) 813 814 if self.__problem is not None: 815 issue = emr.problem2issue(self.__problem) 816 if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True): 817 print("error moving episode to issue") 818 819 epi_id = new_episode['pk_episode'] 820 else: 821 epi_id = self.__problem['pk_episode'] 822 823 # set up clinical context in progress note 824 encounter = emr.active_encounter 825 staff_id = gmStaff.gmCurrentProvider()['pk_staff'] 826 clin_ctx = { 827 gmSOAPimporter.soap_bundle_EPISODE_ID_KEY: epi_id, 828 gmSOAPimporter.soap_bundle_ENCOUNTER_ID_KEY: encounter['pk_encounter'], 829 gmSOAPimporter.soap_bundle_STAFF_ID_KEY: staff_id 830 } 831 for line in progress_note: 832 line[gmSOAPimporter.soap_bundle_CLIN_CTX_KEY] = clin_ctx 833 834 # dump progress note to backend 835 importer = gmSOAPimporter.cSOAPImporter() 836 if not importer.import_soap(progress_note): 837 gmGuiHelpers.gm_show_error(_('Error saving progress note.'), _('saving progress note')) 838 return False 839 840 # dump embedded data to backend 841 if not self.__embedded_data_holder.save(): 842 gmGuiHelpers.gm_show_error ( 843 _('Error saving embedded data.'), 844 _('saving progress note') 845 ) 846 return False 847 self.__embedded_data_holder.clear() 848 849 return True850 #-------------------------------------------------------- 853 #--------------------------------------------------------864 """Basic progress note panel. 865 866 It provides a gmResizingWindow based progress note editor 867 with a header line. The header either displays the episode 868 this progress note is associated with or it allows for 869 entering an episode name. The episode name either names 870 an existing episode or is the name for a new episode. 871 872 This panel knows how to save it's data into the backend. 873 874 Can work as: 875 a) Progress note creation: displays an empty set of soap entries to 876 create a new soap note for the given episode (or unassociated) 877 """ 878 #--------------------------------------------------------988 #============================================================ 993 #============================================================880 """ 881 Construct a new SOAP input widget. 882 883 @param parent: the parent widget 884 885 @param episode: the episode to create the SOAP editor for. 886 @type episode gmEMRStructItems.cEpisode instance or None (to create an 887 unassociated progress note). A gmEMRStructItems.cProblem instance is 888 also allowed to be passed, as the widget will obtain the related cEpisode. 889 890 @param input_defs: the display and associated data for each displayed narrative 891 @type input_defs: a list of cSOAPLineDef instances 892 """ 893 if not isinstance(problem, (gmEMRStructItems.cHealthIssue, gmEMRStructItems.cEpisode, gmEMRStructItems.cProblem, type(None))): 894 raise gmExceptions.ConstructorError('problem [%s] is of type %s, must be issue, episode, problem or None' % (str(problem), type(problem))) 895 896 self.__is_saved = False 897 # do layout 898 wx.Panel.__init__(self, parent, -1, style = wx.NO_BORDER | wx.TAB_TRAVERSAL) 899 # - editor 900 if input_defs is None: 901 soap_lines = [] 902 # make Richard the default ;-) 903 # FIXME: actually, should be read from backend 904 line = cSOAPLineDef() 905 line.label = _('Visit Purpose') 906 line.soap_cat = 's' 907 line.is_rfe = True 908 soap_lines.append(line) 909 910 line = cSOAPLineDef() 911 line.label = _('History Taken') 912 line.soap_cat = 's' 913 soap_lines.append(line) 914 915 line = cSOAPLineDef() 916 line.label = _('Findings') 917 line.soap_cat = 'o' 918 soap_lines.append(line) 919 920 line = cSOAPLineDef() 921 line.label = _('Assessment') 922 line.soap_cat = 'a' 923 soap_lines.append(line) 924 925 line = cSOAPLineDef() 926 line.label = _('Plan') 927 line.soap_cat = 'p' 928 soap_lines.append(line) 929 else: 930 soap_lines = input_defs 931 self.__soap_editor = cResizingSoapWin ( 932 self, 933 size = wx.DefaultSize, 934 input_defs = soap_lines, 935 problem = problem 936 ) 937 # - arrange 938 self.__szr_main = wx.BoxSizer(wx.VERTICAL) 939 self.__szr_main.Add(self.__soap_editor, 1, wx.EXPAND) 940 self.SetSizerAndFit(self.__szr_main)941 #-------------------------------------------------------- 942 # public API 943 #--------------------------------------------------------945 """Retrieve the related problem for this SOAP input widget. 946 """ 947 return self.__soap_editor.get_problem()948 #--------------------------------------------------------950 """ 951 Retrieves whether the current editor is not associated 952 with any episode. 953 """ 954 return ((self.__problem is None) or (self.__problem['type'] == 'issue'))955 #-------------------------------------------------------- 960 #-------------------------------------------------------- 965 #--------------------------------------------------------967 """ 968 Set SOAP input widget saved (dumped to backend) state 969 970 @param is_saved: Flag indicating wether the SOAP has been dumped to 971 persistent backend 972 @type is_saved: boolean 973 """ 974 self.__is_saved = is_saved 975 self.Clear()976 #--------------------------------------------------------978 """ 979 Check SOAP input widget saved (dumped to backend) state 980 """ 981 return self.__is_saved982 #--------------------------------------------------------984 return self.__soap_editor.save()985 #--------------------------------------------------------987 return self.__soap_editor.is_empty()995 """Single Box free text SOAP input. 996 997 This widget was suggested by David Guest on the mailing 998 list. All it does is provide a single multi-line textbox 999 for typing free-text clinical notes which are stored as 1000 Subjective. 1001 """1088 #============================================================ 1089 # main 1090 #------------------------------------------------------------ 1091 if __name__ == "__main__": 1092 1093 import sys 1094 1095 from Gnumed.pycommon import gmPG2 1096 #--------------------------------------------------------1003 wx.Panel.__init__(self, *args, **kwargs) 1004 self.__do_layout() 1005 self.__pat = gmPerson.gmCurrentPatient() 1006 if not self.__register_events(): 1007 raise gmExceptions.ConstructorError('cannot register interests')1008 #--------------------------------------------------------1010 # large box for free-text clinical notes 1011 self.__soap_box = cSingleBoxSOAP ( 1012 self, 1013 -1, 1014 '', 1015 style = wx.TE_MULTILINE 1016 ) 1017 # buttons below that 1018 self.__BTN_save = wx.Button(self, wx.NewId(), _("save")) 1019 self.__BTN_save.SetToolTip(_('save clinical note in EMR')) 1020 self.__BTN_discard = wx.Button(self, wx.NewId(), _("discard")) 1021 self.__BTN_discard.SetToolTip(_('discard clinical note')) 1022 szr_btns = wx.BoxSizer(wx.HORIZONTAL) 1023 szr_btns.Add(self.__BTN_save, 1, wx.ALIGN_CENTER_HORIZONTAL, 0) 1024 szr_btns.Add(self.__BTN_discard, 1, wx.ALIGN_CENTER_HORIZONTAL, 0) 1025 # arrange widgets 1026 szr_outer = wx.StaticBoxSizer(wx.StaticBox(self, -1, _("clinical progress note")), wx.VERTICAL) 1027 szr_outer.Add(self.__soap_box, 1, wx.EXPAND, 0) 1028 szr_outer.Add(szr_btns, 0, wx.EXPAND, 0) 1029 # and do layout 1030 self.SetAutoLayout(1) 1031 self.SetSizer(szr_outer) 1032 szr_outer.Fit(self) 1033 szr_outer.SetSizeHints(self) 1034 self.Layout()1035 #--------------------------------------------------------1037 # wxPython events 1038 self.__BTN_save.Bind(wx.EVT_BUTTON, self._on_save_note) 1039 self.__BTN_discard.Bind(wx.EVT_BUTTON, self._on_discard_note) 1040 1041 # client internal signals 1042 gmDispatcher.connect(signal = 'application_closing', receiver = self._save_note) 1043 # really should be synchronous: 1044 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._save_note) 1045 1046 return True1047 #-------------------------------------------------------- 1048 # event handlers 1049 #-------------------------------------------------------- 1052 #event.Skip() 1053 #-------------------------------------------------------- 1057 #event.Skip() 1058 #-------------------------------------------------------- 1059 # internal helpers 1060 #--------------------------------------------------------1062 # xxxxxx 1063 # FIXME: this should be a sync callback 1064 # xxxxxx 1065 wx.CallAfter(self.__save_note)1066 #--------------------------------------------------------1068 # sanity checks 1069 if self.__pat is None: 1070 return True 1071 if not self.__pat.connected: 1072 return True 1073 if not self.__soap_box.IsModified(): 1074 return True 1075 note = self.__soap_box.GetValue() 1076 if note.strip() == '': 1077 return True 1078 # now save note 1079 emr = self.__pat.emr 1080 if emr is None: 1081 _log.error('cannot access clinical record of patient') 1082 return False 1083 if not emr.add_clin_narrative(note, soap_cat='s'): 1084 _log.error('error saving clinical note') 1085 return False 1086 self.__soap_box.SetValue('') 1087 return True1098 """ 1099 Retrieve the soap editor input lines definitions built from 1100 all the narratives for the given issue along a specific 1101 encounter. 1102 1103 @param pk_health_issue The id of the health issue to obtain the narratives for. 1104 @param pk_health_issue An integer instance 1105 1106 @param pk_encounter The id of the encounter to obtain the narratives for. 1107 @type A gmEMRStructItems.cEncounter instance. 1108 1109 @param default_labels: The user customized labels for each 1110 soap category. 1111 @type default_labels: A dictionary instance which keys are 1112 soap categories. 1113 """ 1114 # custom labels 1115 if default_labels is None: 1116 default_labels = { 1117 's': _('History Taken'), 1118 'o': _('Findings'), 1119 'a': _('Assessment'), 1120 'p': _('Plan') 1121 } 1122 1123 pat = gmPerson.gmCurrentPatient() 1124 emr = pat.emr 1125 soap_lines = [] 1126 # for each soap cat 1127 for soap_cat in gmSOAPimporter.soap_bundle_SOAP_CATS: 1128 # retrieve narrative for given encounter 1129 narr_items = emr.get_clin_narrative ( 1130 encounters = [pk_encounter], 1131 issues = [pk_health_issue], 1132 soap_cats = [soap_cat] 1133 ) 1134 for narrative in narr_items: 1135 try: 1136 # FIXME: add more data such as doctor sig 1137 label_txt = default_labels[narrative['soap_cat']] 1138 except: 1139 label_txt = narrative['soap_cat'] 1140 line = cSOAPLineDef() 1141 line.label = label_txt 1142 line.text = narrative['narrative'] 1143 # line.data['narrative instance'] = narrative 1144 soap_lines.append(line) 1145 return soap_lines1146 #--------------------------------------------------------1148 print("test keyword must have been typed...") 1149 print("actually this would have to return a suitable wx.Window subclass instance") 1150 print("args:", args) 1151 print("kwd args:") 1152 for key in kwargs.keys(): 1153 print(key, "->", kwargs[key])1154 #--------------------------------------------------------1156 msg = ( 1157 "test keyword must have been typed...\n" 1158 "actually this would have to return a suitable wx.Window subclass instance\n" 1159 ) 1160 for arg in args: 1161 msg = msg + "\narg ==> %s" % arg 1162 for key in kwargs.keys(): 1163 msg = msg + "\n%s ==> %s" % (key, kwargs[key]) 1164 gmGuiHelpers.gm_show_info ( 1165 aMessage = msg, 1166 aTitle = 'msg box on create_widget from test_keyword' 1167 )1168 #--------------------------------------------------------1170 print('testing notebooked soap input...') 1171 application = wx.PyWidgetTester(size=(800,500)) 1172 soap_input = cProgressNoteInputNotebook(application.frame, -1) 1173 application.frame.Show(True) 1174 application.MainLoop()1175 #--------------------------------------------------------1177 print('testing notebooked soap panel...') 1178 application = wx.PyWidgetTester(size=(800,500)) 1179 soap_input = cNotebookedProgressNoteInputPanel(application.frame, -1) 1180 application.frame.Show(True) 1181 application.MainLoop()1182 #-------------------------------------------------------- 1183 1184 try: 1185 # obtain patient 1186 patient = gmPersonSearch.ask_for_patient() 1187 if patient is None: 1188 print("No patient. Exiting gracefully...") 1189 sys.exit(0) 1190 gmPatSearchWidgets.set_active_patient(patient=patient) 1191 1192 #test_soap_notebook() 1193 test_soap_notebook_panel() 1194 1195 # # multisash soap 1196 # print 'testing multisashed soap input...' 1197 # application = wx.PyWidgetTester(size=(800,500)) 1198 # soap_input = cMultiSashedProgressNoteInputPanel(application.frame, -1) 1199 # application.frame.Show(True) 1200 # application.MainLoop() 1201 1202 # # soap widget displaying all narratives for an issue along an encounter 1203 # print 'testing soap editor for encounter narratives...' 1204 # episode = gmEMRStructItems.cEpisode(aPK_obj=1) 1205 # encounter = gmEMRStructItems.cEncounter(aPK_obj=1) 1206 # narrative = get_narrative(pk_encounter = encounter['pk_encounter'], pk_health_issue = episode['pk_health_issue']) 1207 # default_labels = {'s':'Subjective', 'o':'Objective', 'a':'Assesment', 'p':'Plan'} 1208 # app = wx.PyWidgetTester(size=(300,500)) 1209 # app.SetWidget(cResizingSoapPanel, episode, narrative) 1210 # app.MainLoop() 1211 # del app 1212 1213 # # soap progress note for episode 1214 # print 'testing soap editor for episode...' 1215 # app = wx.PyWidgetTester(size=(300,300)) 1216 # app.SetWidget(cResizingSoapPanel, episode) 1217 # app.MainLoop() 1218 # del app 1219 1220 # # soap progress note for problem 1221 # print 'testing soap editor for problem...' 1222 # problem = gmEMRStructItems.cProblem(aPK_obj={'pk_patient': 12, 'pk_health_issue': 1, 'pk_episode': 1}) 1223 # app = wx.PyWidgetTester(size=(300,300)) 1224 # app.SetWidget(cResizingSoapPanel, problem) 1225 # app.MainLoop() 1226 # del app 1227 1228 # # unassociated soap progress note 1229 # print 'testing unassociated soap editor...' 1230 # app = wx.PyWidgetTester(size=(300,300)) 1231 # app.SetWidget(cResizingSoapPanel, None) 1232 # app.MainLoop() 1233 # del app 1234 1235 # # unstructured progress note 1236 # print 'testing unstructured progress note...' 1237 # app = wx.PyWidgetTester(size=(600,600)) 1238 # app.SetWidget(cSingleBoxSOAPPanel, -1) 1239 # app.MainLoop() 1240 1241 except Exception: 1242 _log.exception("unhandled exception caught !") 1243 # but re-raise them 1244 raise 1245 1246 #============================================================ 1247
Home | Trees | Indices | Help |
|
---|
Generated by Epydoc 3.0.1 on Sun Aug 19 01:55:20 2018 | http://epydoc.sourceforge.net |