| 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 popup
43 #============================================================
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 popup
64 #============================================================
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 """
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 result
217 #--------------------------------------------------------
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 True
253 #--------------------------------------------------------
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 True
305 #--------------------------------------------------------
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()
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 #============================================================
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 #--------------------------------------------------------
371 """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 True
513 #--------------------------------------------------------
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 False
609 #--------------------------------------------------------
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 True
623 #--------------------------------------------------------
624 # notebook plugin API
625 #--------------------------------------------------------
628 #============================================================
636 #============================================================
637 # FIXME: this should be a more generic(ally named) class
638 # FIXME: living elsewhere
640
641 _data_savers = {}
642
645 #--------------------------------------------------------
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 True
668 #--------------------------------------------------------
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 True
680 #--------------------------------------------------------
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 #============================================================
692
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 = None
758 #--------------------------------------------------------
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 True
850 #--------------------------------------------------------
853 #--------------------------------------------------------
862 #============================================================
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 #--------------------------------------------------------
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_saved
982 #--------------------------------------------------------
984 return self.__soap_editor.save()
985 #--------------------------------------------------------
987 return self.__soap_editor.is_empty()
988 #============================================================
993 #============================================================
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 """
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 True
1047 #--------------------------------------------------------
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 True
1088 #============================================================
1089 # main
1090 #------------------------------------------------------------
1091 if __name__ == "__main__":
1092
1093 import sys
1094
1095 from Gnumed.pycommon import gmPG2
1096 #--------------------------------------------------------
1098 """
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_lines
1146 #--------------------------------------------------------
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 Fri Jan 25 02:55:27 2019 | http://epydoc.sourceforge.net |