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
51
53
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
63 self._LCTRL_items.set_columns([_('when'), _('who'), _('type'), _('entry')])
64
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
75
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
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 """
152
153
154
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
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
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
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(initial = epi['health_issue'], instead = '?')
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
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
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
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
365
367 """Configure enabled event signals."""
368
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
379 self._schedule_data_reget()
380 self._PNL_editors.patient = self.__pat
381
383 self._schedule_data_reget()
384
385
386
388 """Show related note at the bottom."""
389 pass
390
393
394
397
398
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
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
432 self.__refresh_problem_list()
433
435 self.__refresh_problem_list()
436
437
438
440 self.__refresh_recent_notes (
441 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
442 )
443
445 self.__refresh_recent_notes (
446 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
447 )
448
449
450
451
452
453
454
456 self.__refresh_problem_list()
457 return True
458
459
460 from Gnumed.wxGladeWidgets import wxgFancySoapEditorPnl
461
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 """
480
481
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
488
490
491 self.__pat = patient
492 self.__refresh_encounter()
493 self.__refresh_soap_notebook()
494
495 patient = property(_get_patient, _set_patient)
496
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
526
528 self._NB_soap_editors.MoveAfterInTabOrder(self._PRW_aoe_codes)
529
531 self._NB_soap_editors.DeleteAllPages()
532 self._NB_soap_editors.add_editor()
533
558
559
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
589
590
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
617
618
619
621 """Configure enabled event signals."""
622
623 gmDispatcher.send(signal = 'register_pre_exit_callback', callback = self._pre_exit_callback)
624
625
626 gmDispatcher.connect(signal = 'blobs.doc_med_mod_db', receiver = self._on_doc_mod_db)
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
638
639 if self.__pat is None:
640 return True
641 return self._NB_soap_editors.warn_on_unsaved_soap()
642
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
652
653
654
655
656
657
658
659
660
661
662
663
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
677 self.__refresh_current_editor()
678
682
684 self.__refresh_encounter()
685
687 self.__refresh_encounter()
688
689
690
694
698
702
712
732
736
737
738
742
743
744
753
765
766
975
976
977
978 from Gnumed.wxGladeWidgets import wxgSimpleSoapPluginPnl
979
980 -class cSimpleSoapPluginPnl(wxgSimpleSoapPluginPnl.wxgSimpleSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
990
991
992
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
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
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
1038 if self._TCTRL_soap.GetValue().strip() == '':
1039 return True
1040 if self.__problem is None:
1041
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
1062
1083
1084
1085
1087 """Configure enabled event signals."""
1088
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
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
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
1122
1124 self._schedule_data_reget()
1125
1127 self._schedule_data_reget()
1128
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
1157
1159 event.Skip()
1160 self.__refresh_journal()
1161
1163 event.Skip()
1164 self.__refresh_journal()
1165
1180
1187
1195
1199
1203
1204
1205
1207 self.__refresh_problem_list()
1208 self.__refresh_journal()
1209 self._TCTRL_soap.SetValue('')
1210 return True
1211
1212
1213
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
1239
1240
1241