1 """GNUmed patient EMR tree browser.
2 """
3
4 __version__ = "$Revision: 1.111 $"
5 __author__ = "cfmoro1976@yahoo.es, sjtan@swiftdsl.com.au, Karsten.Hilbert@gmx.net"
6 __license__ = "GPL"
7
8
9 import sys, types, os.path, StringIO, codecs, logging
10
11
12
13 import wx
14
15
16
17 from Gnumed.pycommon import gmI18N, gmDispatcher, gmExceptions, gmTools
18 from Gnumed.exporters import gmPatientExporter
19 from Gnumed.business import gmEMRStructItems, gmPerson, gmSOAPimporter
20 from Gnumed.wxpython import gmGuiHelpers, gmEMRStructWidgets, gmSOAPWidgets
21 from Gnumed.wxpython import gmAllergyWidgets, gmNarrativeWidgets, gmPatSearchWidgets
22 from Gnumed.wxpython import gmDemographicsWidgets, gmVaccWidgets
23
24
25 _log = logging.getLogger('gm.ui')
26 _log.info(__version__)
27
28
30 """
31 Dump the patient's EMR from GUI client
32 @param parent - The parent widget
33 @type parent - A wx.Window instance
34 """
35
36 if parent is None:
37 raise TypeError('expected wx.Window instance as parent, got <None>')
38
39 pat = gmPerson.gmCurrentPatient()
40 if not pat.connected:
41 gmDispatcher.send(signal='statustext', msg=_('Cannot export EMR. No active patient.'))
42 return False
43
44
45 wc = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files"))
46 defdir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed', 'export', 'EMR', pat['dirname'])))
47 gmTools.mkdir(defdir)
48 fname = '%s-%s_%s.txt' % (_('emr-export'), pat['lastnames'], pat['firstnames'])
49 dlg = wx.FileDialog (
50 parent = parent,
51 message = _("Save patient's EMR as..."),
52 defaultDir = defdir,
53 defaultFile = fname,
54 wildcard = wc,
55 style = wx.SAVE
56 )
57 choice = dlg.ShowModal()
58 fname = dlg.GetPath()
59 dlg.Destroy()
60 if choice != wx.ID_OK:
61 return None
62
63 _log.debug('exporting EMR to [%s]', fname)
64
65
66 output_file = codecs.open(fname, 'wb', encoding='utf8', errors='replace')
67 exporter = gmPatientExporter.cEmrExport(patient = pat)
68 exporter.set_output_file(output_file)
69 exporter.dump_constraints()
70 exporter.dump_demographic_record(True)
71 exporter.dump_clinical_record()
72 exporter.dump_med_docs()
73 output_file.close()
74
75 gmDispatcher.send('statustext', msg = _('EMR successfully exported to file: %s') % fname, beep = False)
76 return fname
77
78 -class cEMRTree(wx.TreeCtrl, gmGuiHelpers.cTreeExpansionHistoryMixin):
79 """This wx.TreeCtrl derivative displays a tree view of the medical record."""
80
81
82 - def __init__(self, parent, id, *args, **kwds):
83 """Set up our specialised tree.
84 """
85 kwds['style'] = wx.TR_HAS_BUTTONS | wx.NO_BORDER
86 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds)
87
88 gmGuiHelpers.cTreeExpansionHistoryMixin.__init__(self)
89
90 try:
91 self.__narr_display = kwds['narr_display']
92 del kwds['narr_display']
93 except KeyError:
94 self.__narr_display = None
95 self.__pat = gmPerson.gmCurrentPatient()
96 self.__curr_node = None
97 self.__exporter = gmPatientExporter.cEmrExport(patient = self.__pat)
98
99 self._old_cursor_pos = None
100
101 self.__make_popup_menus()
102 self.__register_events()
103
104
105
107 if not self.__pat.connected:
108 gmDispatcher.send(signal='statustext', msg=_('Cannot load clinical narrative. No active patient.'),)
109 return False
110
111 if not self.__populate_tree():
112 return False
113
114 return True
115
117 self.__narr_display = narrative_display
118
119
120
122 """Configures enabled event signals."""
123 wx.EVT_TREE_SEL_CHANGED (self, self.GetId(), self._on_tree_item_selected)
124 wx.EVT_TREE_ITEM_RIGHT_CLICK (self, self.GetId(), self._on_tree_item_right_clicked)
125
126
127
128 wx.EVT_TREE_ITEM_GETTOOLTIP(self, -1, self._on_tree_item_gettooltip)
129
130 gmDispatcher.connect(signal = 'narrative_mod_db', receiver = self._on_narrative_mod_db)
131 gmDispatcher.connect(signal = 'episode_mod_db', receiver = self._on_episode_mod_db)
132 gmDispatcher.connect(signal = 'health_issue_mod_db', receiver = self._on_issue_mod_db)
133
135 """Updates EMR browser data."""
136
137
138
139 wx.BeginBusyCursor()
140
141 self.snapshot_expansion()
142
143
144 self.DeleteAllItems()
145 root_item = self.AddRoot(_('EMR of %s') % self.__pat['description'])
146 self.SetPyData(root_item, None)
147 self.SetItemHasChildren(root_item, True)
148
149
150 self.__exporter.get_historical_tree(self)
151 self.__curr_node = root_item
152
153 self.SelectItem(root_item)
154 self.Expand(root_item)
155 self.__update_text_for_selected_node()
156
157 self.restore_expansion()
158
159 wx.EndBusyCursor()
160 return True
161
163 """Displays information for the selected tree node."""
164
165 if self.__narr_display is None:
166 return
167
168 if self.__curr_node is None:
169 return
170
171 node_data = self.GetPyData(self.__curr_node)
172
173
174 if isinstance(node_data, (gmEMRStructItems.cHealthIssue, types.DictType)):
175
176 if node_data['pk_health_issue'] is None:
177 txt = _('Pool of unassociated episodes:\n\n "%s"') % node_data['description']
178 else:
179 txt = node_data.format(left_margin=1, patient = self.__pat)
180
181 elif isinstance(node_data, gmEMRStructItems.cEpisode):
182 txt = node_data.format(left_margin = 1, patient = self.__pat)
183
184 elif isinstance(node_data, gmEMRStructItems.cEncounter):
185 epi = self.GetPyData(self.GetItemParent(self.__curr_node))
186 txt = node_data.format(episodes = [epi['pk_episode']], with_soap = True, left_margin = 1, patient = self.__pat)
187
188 else:
189 emr = self.__pat.get_emr()
190 txt = emr.format_summary()
191
192 self.__narr_display.Clear()
193 self.__narr_display.WriteText(txt)
194
196
197
198 self.__epi_context_popup = wx.Menu(title = _('Episode Menu'))
199
200 menu_id = wx.NewId()
201 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Edit details')))
202 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__edit_episode)
203
204 menu_id = wx.NewId()
205 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Delete')))
206 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__delete_episode)
207
208 menu_id = wx.NewId()
209 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Promote')))
210 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__promote_episode_to_issue)
211
212 menu_id = wx.NewId()
213 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Move encounters')))
214 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__move_encounters)
215
216
217 self.__enc_context_popup = wx.Menu(title = _('Encounter Menu'))
218
219 menu_id = wx.NewId()
220 self.__enc_context_popup.AppendItem(wx.MenuItem(self.__enc_context_popup, menu_id, _('Move data to another episode')))
221 wx.EVT_MENU(self.__enc_context_popup, menu_id, self.__relink_encounter_data2episode)
222
223 menu_id = wx.NewId()
224 self.__enc_context_popup.AppendItem(wx.MenuItem(self.__enc_context_popup, menu_id, _('Edit details')))
225 wx.EVT_MENU(self.__enc_context_popup, menu_id, self.__edit_encounter_details)
226
227 item = self.__enc_context_popup.Append(-1, _('Edit progress notes'))
228 self.Bind(wx.EVT_MENU, self.__edit_progress_notes, item)
229
230 item = self.__enc_context_popup.Append(-1, _('Move progress notes'))
231 self.Bind(wx.EVT_MENU, self.__move_progress_notes, item)
232
233 item = self.__enc_context_popup.Append(-1, _('Export for Medistar'))
234 self.Bind(wx.EVT_MENU, self.__export_encounter_for_medistar, item)
235
236
237 self.__issue_context_popup = wx.Menu(title = _('Health Issue Menu'))
238
239 menu_id = wx.NewId()
240 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Edit details')))
241 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__edit_issue)
242
243 menu_id = wx.NewId()
244 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Delete')))
245 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__delete_issue)
246
247 self.__issue_context_popup.AppendSeparator()
248
249 menu_id = wx.NewId()
250 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Open to encounter level')))
251 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__expand_issue_to_encounter_level)
252
253
254
255
256 self.__root_context_popup = wx.Menu(title = _('EMR Menu'))
257
258 menu_id = wx.NewId()
259 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Create health issue')))
260 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__create_issue)
261
262 menu_id = wx.NewId()
263 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage allergies')))
264 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__document_allergy)
265
266 menu_id = wx.NewId()
267 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage vaccinations')))
268 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_vaccinations)
269
270 menu_id = wx.NewId()
271 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage procedures')))
272 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_procedures)
273
274 menu_id = wx.NewId()
275 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage hospitalizations')))
276 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_hospital_stays)
277
278 menu_id = wx.NewId()
279 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage occupation')))
280 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_occupation)
281
282 self.__root_context_popup.AppendSeparator()
283
284
285 expand_menu = wx.Menu()
286 self.__root_context_popup.AppendMenu(wx.NewId(), _('Open EMR to ...'), expand_menu)
287
288 menu_id = wx.NewId()
289 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... issue level')))
290 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_issue_level)
291
292 menu_id = wx.NewId()
293 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... episode level')))
294 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_episode_level)
295
296 menu_id = wx.NewId()
297 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... encounter level')))
298 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_encounter_level)
299
300 - def __handle_root_context(self, pos=wx.DefaultPosition):
301 self.PopupMenu(self.__root_context_popup, pos)
302
303 - def __handle_issue_context(self, pos=wx.DefaultPosition):
304
305 self.PopupMenu(self.__issue_context_popup, pos)
306
307 - def __handle_episode_context(self, pos=wx.DefaultPosition):
308 self.__epi_context_popup.SetTitle(_('Episode %s') % self.__curr_node_data['description'])
309 self.PopupMenu(self.__epi_context_popup, pos)
310
311 - def __handle_encounter_context(self, pos=wx.DefaultPosition):
312 self.PopupMenu(self.__enc_context_popup, pos)
313
314
315
324
327
331
333 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
334 parent = self,
335 id = -1,
336 caption = _('Deleting episode'),
337 button_defs = [
338 {'label': _('Yes, delete'), 'tooltip': _('Delete the episode if possible (it must be completely empty).')},
339 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the episode.')}
340 ],
341 question = _(
342 'Are you sure you want to delete this episode ?\n'
343 '\n'
344 ' "%s"\n'
345 ) % self.__curr_node_data['description']
346 )
347 result = dlg.ShowModal()
348 if result != wx.ID_YES:
349 return
350
351 try:
352 gmEMRStructItems.delete_episode(episode = self.__curr_node_data)
353 except gmExceptions.DatabaseObjectInUseError:
354 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete episode. There is still clinical data recorded for it.'))
355 return
356
357
358
360 encounter = self.GetPyData(self.__curr_node)
361 node_parent = self.GetItemParent(self.__curr_node)
362 episode = self.GetPyData(node_parent)
363
364 gmNarrativeWidgets.move_progress_notes_to_another_encounter (
365 parent = self,
366 encounters = [encounter['pk_encounter']],
367 episodes = [episode['pk_episode']]
368 )
369
371 encounter = self.GetPyData(self.__curr_node)
372 node_parent = self.GetItemParent(self.__curr_node)
373 episode = self.GetPyData(node_parent)
374
375 gmNarrativeWidgets.manage_progress_notes (
376 parent = self,
377 encounters = [encounter['pk_encounter']],
378 episodes = [episode['pk_episode']]
379 )
380
387
389
390 node_parent = self.GetItemParent(self.__curr_node)
391 owning_episode = self.GetPyData(node_parent)
392
393 episode_selector = gmNarrativeWidgets.cMoveNarrativeDlg (
394 self,
395 -1,
396 episode = owning_episode,
397 encounter = self.__curr_node_data
398 )
399
400 result = episode_selector.ShowModal()
401 episode_selector.Destroy()
402
403 if result == wx.ID_YES:
404 self.__populate_tree()
405
406
407
410
412 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
413 parent = self,
414 id = -1,
415 caption = _('Deleting health issue'),
416 button_defs = [
417 {'label': _('Yes, delete'), 'tooltip': _('Delete the health issue if possible (it must be completely empty).')},
418 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the health issue.')}
419 ],
420 question = _(
421 'Are you sure you want to delete this health issue ?\n'
422 '\n'
423 ' "%s"\n'
424 ) % self.__curr_node_data['description']
425 )
426 result = dlg.ShowModal()
427 if result != wx.ID_YES:
428 dlg.Destroy()
429 return
430
431 dlg.Destroy()
432
433 try:
434 gmEMRStructItems.delete_health_issue(health_issue = self.__curr_node_data)
435 except gmExceptions.DatabaseObjectInUseError:
436 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete health issue. There is still clinical data recorded for it.'))
437
439
440 if not self.__curr_node.IsOk():
441 return
442
443 self.Expand(self.__curr_node)
444
445 epi, epi_cookie = self.GetFirstChild(self.__curr_node)
446 while epi.IsOk():
447 self.Expand(epi)
448 epi, epi_cookie = self.GetNextChild(self.__curr_node, epi_cookie)
449
450
451
454
462
465
468
471
474
476
477 root_item = self.GetRootItem()
478
479 if not root_item.IsOk():
480 return
481
482 self.Expand(root_item)
483
484
485 issue, issue_cookie = self.GetFirstChild(root_item)
486 while issue.IsOk():
487 self.Collapse(issue)
488 epi, epi_cookie = self.GetFirstChild(issue)
489 while epi.IsOk():
490 self.Collapse(epi)
491 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
492 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
493
495
496 root_item = self.GetRootItem()
497
498 if not root_item.IsOk():
499 return
500
501 self.Expand(root_item)
502
503
504 issue, issue_cookie = self.GetFirstChild(root_item)
505 while issue.IsOk():
506 self.Expand(issue)
507 epi, epi_cookie = self.GetFirstChild(issue)
508 while epi.IsOk():
509 self.Collapse(epi)
510 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
511 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
512
514
515 root_item = self.GetRootItem()
516
517 if not root_item.IsOk():
518 return
519
520 self.Expand(root_item)
521
522
523 issue, issue_cookie = self.GetFirstChild(root_item)
524 while issue.IsOk():
525 self.Expand(issue)
526 epi, epi_cookie = self.GetFirstChild(issue)
527 while epi.IsOk():
528 self.Expand(epi)
529 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
530 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
531
538
539
540
542 wx.CallAfter(self.__update_text_for_selected_node)
543
545 wx.CallAfter(self.__populate_tree)
546
548 wx.CallAfter(self.__populate_tree)
549
551 sel_item = event.GetItem()
552 self.__curr_node = sel_item
553 self.__update_text_for_selected_node()
554 return True
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
642
643
644
645
646
647
648
649
650
651
652
653
655 """Right button clicked: display the popup for the tree"""
656
657 node = event.GetItem()
658 self.SelectItem(node)
659 self.__curr_node_data = self.GetPyData(node)
660 self.__curr_node = node
661
662 pos = wx.DefaultPosition
663 if isinstance(self.__curr_node_data, gmEMRStructItems.cHealthIssue):
664 self.__handle_issue_context(pos=pos)
665 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEpisode):
666 self.__handle_episode_context(pos=pos)
667 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEncounter):
668 self.__handle_encounter_context(pos=pos)
669 elif node == self.GetRootItem():
670 self.__handle_root_context()
671 elif type(self.__curr_node_data) == type({}):
672
673 pass
674 else:
675 print "error: unknown node type, no popup menu"
676 event.Skip()
677
679 """Used in sorting items.
680
681 -1: 1 < 2
682 0: 1 = 2
683 1: 1 > 2
684 """
685
686
687 item1 = self.GetPyData(node1)
688 item2 = self.GetPyData(node2)
689
690
691 if isinstance(item1, gmEMRStructItems.cEncounter):
692 if item1['started'] == item2['started']:
693 return 0
694 if item1['started'] > item2['started']:
695 return -1
696 return 1
697
698
699 if isinstance(item1, gmEMRStructItems.cEpisode):
700 start1 = item1.get_access_range()[0]
701 start2 = item2.get_access_range()[0]
702 if start1 == start2:
703 return 0
704 if start1 < start2:
705 return -1
706 return 1
707
708
709 if isinstance(item1, gmEMRStructItems.cHealthIssue):
710
711
712 if item1['grouping'] is None:
713 if item2['grouping'] is not None:
714 return 1
715
716
717 if item1['grouping'] is not None:
718 if item2['grouping'] is None:
719 return -1
720
721
722 if (item1['grouping'] is None) and (item2['grouping'] is None):
723 if item1['description'].lower() < item2['description'].lower():
724 return -1
725 if item1['description'].lower() > item2['description'].lower():
726 return 1
727 return 0
728
729
730 if item1['grouping'] < item2['grouping']:
731 return -1
732
733 if item1['grouping'] > item2['grouping']:
734 return 1
735
736 if item1['description'].lower() < item2['description'].lower():
737 return -1
738
739 if item1['description'].lower() > item2['description'].lower():
740 return 1
741
742 return 0
743
744
745 if isinstance(item1, type({})):
746 return -1
747
748 return 0
749
750 from Gnumed.wxGladeWidgets import wxgScrolledEMRTreePnl
751
769
770 from Gnumed.wxGladeWidgets import wxgSplittedEMRTreeBrowserPnl
771
773 """A splitter window holding an EMR tree.
774
775 The left hand side displays a scrollable EMR tree while
776 on the right details for selected items are displayed.
777
778 Expects to be put into a Notebook.
779 """
784
786 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
787 return True
788
790 if self.GetParent().GetCurrentPage() == self:
791 self.repopulate_ui()
792 return True
793
795 """Fills UI with data."""
796 self._pnl_emr_tree.repopulate_ui()
797 self._splitter_browser.SetSashPosition(self._splitter_browser.GetSizeTuple()[0]/3, True)
798 return True
799
802 wx.Panel.__init__(self, *args, **kwargs)
803
804 self.__do_layout()
805 self.__register_events()
806
808 self.__journal = wx.TextCtrl (
809 self,
810 -1,
811 _('No EMR data loaded.'),
812 style = wx.TE_MULTILINE | wx.TE_READONLY
813 )
814 self.__journal.SetFont(wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL))
815
816 szr_outer = wx.BoxSizer(wx.VERTICAL)
817 szr_outer.Add(self.__journal, 1, wx.EXPAND, 0)
818
819 self.SetAutoLayout(1)
820 self.SetSizer(szr_outer)
821 szr_outer.Fit(self)
822 szr_outer.SetSizeHints(self)
823 self.Layout()
824
826 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
827
829 """Expects to be in a Notebook."""
830 if self.GetParent().GetCurrentPage() == self:
831 self.repopulate_ui()
832 return True
833
834
835
837 txt = StringIO.StringIO()
838 exporter = gmPatientExporter.cEMRJournalExporter()
839
840
841 try:
842 exporter.export(txt)
843 self.__journal.SetValue(txt.getvalue())
844 except ValueError:
845 _log.exception('cannot get EMR journal')
846 self.__journal.SetValue (_(
847 'An error occurred while retrieving the EMR\n'
848 'in journal form for the active patient.\n\n'
849 'Please check the log file for details.'
850 ))
851 txt.close()
852 self.__journal.ShowPosition(self.__journal.GetLastPosition())
853 return True
854
855
856
857 if __name__ == '__main__':
858
859 _log.info("starting emr browser...")
860
861 try:
862
863 patient = gmPerson.ask_for_patient()
864 if patient is None:
865 print "No patient. Exiting gracefully..."
866 sys.exit(0)
867 gmPatSearchWidgets.set_active_patient(patient = patient)
868
869
870 application = wx.PyWidgetTester(size=(800,600))
871 emr_browser = cEMRBrowserPanel(application.frame, -1)
872 emr_browser.refresh_tree()
873
874 application.frame.Show(True)
875 application.MainLoop()
876
877
878 if patient is not None:
879 try:
880 patient.cleanup()
881 except:
882 print "error cleaning up patient"
883 except StandardError:
884 _log.exception("unhandled exception caught !")
885
886 raise
887
888 _log.info("closing emr browser...")
889
890
891