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
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 procedures')))
268 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_procedures)
269
270 menu_id = wx.NewId()
271 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage hospitalizations')))
272 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_hospital_stays)
273
274 menu_id = wx.NewId()
275 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage occupation')))
276 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_occupation)
277
278 self.__root_context_popup.AppendSeparator()
279
280
281 expand_menu = wx.Menu()
282 self.__root_context_popup.AppendMenu(wx.NewId(), _('Open EMR to ...'), expand_menu)
283
284 menu_id = wx.NewId()
285 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... issue level')))
286 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_issue_level)
287
288 menu_id = wx.NewId()
289 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... episode level')))
290 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_episode_level)
291
292 menu_id = wx.NewId()
293 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... encounter level')))
294 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_encounter_level)
295
296 - def __handle_root_context(self, pos=wx.DefaultPosition):
297 self.PopupMenu(self.__root_context_popup, pos)
298
299 - def __handle_issue_context(self, pos=wx.DefaultPosition):
300
301 self.PopupMenu(self.__issue_context_popup, pos)
302
303 - def __handle_episode_context(self, pos=wx.DefaultPosition):
304 self.__epi_context_popup.SetTitle(_('Episode %s') % self.__curr_node_data['description'])
305 self.PopupMenu(self.__epi_context_popup, pos)
306
307 - def __handle_encounter_context(self, pos=wx.DefaultPosition):
308 self.PopupMenu(self.__enc_context_popup, pos)
309
310
311
320
323
327
329 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
330 parent = self,
331 id = -1,
332 caption = _('Deleting episode'),
333 button_defs = [
334 {'label': _('Yes, delete'), 'tooltip': _('Delete the episode if possible (it must be completely empty).')},
335 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the episode.')}
336 ],
337 question = _(
338 'Are you sure you want to delete this episode ?\n'
339 '\n'
340 ' "%s"\n'
341 ) % self.__curr_node_data['description']
342 )
343 result = dlg.ShowModal()
344 if result != wx.ID_YES:
345 return
346
347 try:
348 gmEMRStructItems.delete_episode(episode = self.__curr_node_data)
349 except gmExceptions.DatabaseObjectInUseError:
350 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete episode. There is still clinical data recorded for it.'))
351 return
352
353
354
356 encounter = self.GetPyData(self.__curr_node)
357 node_parent = self.GetItemParent(self.__curr_node)
358 episode = self.GetPyData(node_parent)
359
360 gmNarrativeWidgets.move_progress_notes_to_another_encounter (
361 parent = self,
362 encounters = [encounter['pk_encounter']],
363 episodes = [episode['pk_episode']]
364 )
365
367 encounter = self.GetPyData(self.__curr_node)
368 node_parent = self.GetItemParent(self.__curr_node)
369 episode = self.GetPyData(node_parent)
370
371 gmNarrativeWidgets.manage_progress_notes (
372 parent = self,
373 encounters = [encounter['pk_encounter']],
374 episodes = [episode['pk_episode']]
375 )
376
383
385
386 node_parent = self.GetItemParent(self.__curr_node)
387 owning_episode = self.GetPyData(node_parent)
388
389 episode_selector = gmNarrativeWidgets.cMoveNarrativeDlg (
390 self,
391 -1,
392 episode = owning_episode,
393 encounter = self.__curr_node_data
394 )
395
396 result = episode_selector.ShowModal()
397 episode_selector.Destroy()
398
399 if result == wx.ID_YES:
400 self.__populate_tree()
401
402
403
406
408 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
409 parent = self,
410 id = -1,
411 caption = _('Deleting health issue'),
412 button_defs = [
413 {'label': _('Yes, delete'), 'tooltip': _('Delete the health issue if possible (it must be completely empty).')},
414 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the health issue.')}
415 ],
416 question = _(
417 'Are you sure you want to delete this health issue ?\n'
418 '\n'
419 ' "%s"\n'
420 ) % self.__curr_node_data['description']
421 )
422 result = dlg.ShowModal()
423 if result != wx.ID_YES:
424 dlg.Destroy()
425 return
426
427 dlg.Destroy()
428
429 try:
430 gmEMRStructItems.delete_health_issue(health_issue = self.__curr_node_data)
431 except gmExceptions.DatabaseObjectInUseError:
432 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete health issue. There is still clinical data recorded for it.'))
433
435
436 if not self.__curr_node.IsOk():
437 return
438
439 self.Expand(self.__curr_node)
440
441 epi, epi_cookie = self.GetFirstChild(self.__curr_node)
442 while epi.IsOk():
443 self.Expand(epi)
444 epi, epi_cookie = self.GetNextChild(self.__curr_node, epi_cookie)
445
446
447
450
458
461
464
467
469
470 root_item = self.GetRootItem()
471
472 if not root_item.IsOk():
473 return
474
475 self.Expand(root_item)
476
477
478 issue, issue_cookie = self.GetFirstChild(root_item)
479 while issue.IsOk():
480 self.Collapse(issue)
481 epi, epi_cookie = self.GetFirstChild(issue)
482 while epi.IsOk():
483 self.Collapse(epi)
484 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
485 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
486
488
489 root_item = self.GetRootItem()
490
491 if not root_item.IsOk():
492 return
493
494 self.Expand(root_item)
495
496
497 issue, issue_cookie = self.GetFirstChild(root_item)
498 while issue.IsOk():
499 self.Expand(issue)
500 epi, epi_cookie = self.GetFirstChild(issue)
501 while epi.IsOk():
502 self.Collapse(epi)
503 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
504 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
505
507
508 root_item = self.GetRootItem()
509
510 if not root_item.IsOk():
511 return
512
513 self.Expand(root_item)
514
515
516 issue, issue_cookie = self.GetFirstChild(root_item)
517 while issue.IsOk():
518 self.Expand(issue)
519 epi, epi_cookie = self.GetFirstChild(issue)
520 while epi.IsOk():
521 self.Expand(epi)
522 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
523 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
524
531
532
533
535 wx.CallAfter(self.__update_text_for_selected_node)
536
538 wx.CallAfter(self.__populate_tree)
539
541 wx.CallAfter(self.__populate_tree)
542
544 sel_item = event.GetItem()
545 self.__curr_node = sel_item
546 self.__update_text_for_selected_node()
547 return True
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
635
636
637
638
639
640
641
642
643
644
645
646
648 """Right button clicked: display the popup for the tree"""
649
650 node = event.GetItem()
651 self.SelectItem(node)
652 self.__curr_node_data = self.GetPyData(node)
653 self.__curr_node = node
654
655 pos = wx.DefaultPosition
656 if isinstance(self.__curr_node_data, gmEMRStructItems.cHealthIssue):
657 self.__handle_issue_context(pos=pos)
658 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEpisode):
659 self.__handle_episode_context(pos=pos)
660 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEncounter):
661 self.__handle_encounter_context(pos=pos)
662 elif node == self.GetRootItem():
663 self.__handle_root_context()
664 elif type(self.__curr_node_data) == type({}):
665
666 pass
667 else:
668 print "error: unknown node type, no popup menu"
669 event.Skip()
670
672 """Used in sorting items.
673
674 -1: 1 < 2
675 0: 1 = 2
676 1: 1 > 2
677 """
678
679
680 item1 = self.GetPyData(node1)
681 item2 = self.GetPyData(node2)
682
683
684 if isinstance(item1, gmEMRStructItems.cEncounter):
685 if item1['started'] == item2['started']:
686 return 0
687 if item1['started'] > item2['started']:
688 return -1
689 return 1
690
691
692 if isinstance(item1, gmEMRStructItems.cEpisode):
693 start1 = item1.get_access_range()[0]
694 start2 = item2.get_access_range()[0]
695 if start1 == start2:
696 return 0
697 if start1 < start2:
698 return -1
699 return 1
700
701
702 if isinstance(item1, gmEMRStructItems.cHealthIssue):
703
704
705 if item1['grouping'] is None:
706 if item2['grouping'] is not None:
707 return 1
708
709
710 if item1['grouping'] is not None:
711 if item2['grouping'] is None:
712 return -1
713
714
715 if (item1['grouping'] is None) and (item2['grouping'] is None):
716 if item1['description'].lower() < item2['description'].lower():
717 return -1
718 if item1['description'].lower() > item2['description'].lower():
719 return 1
720 return 0
721
722
723 if item1['grouping'] < item2['grouping']:
724 return -1
725
726 if item1['grouping'] > item2['grouping']:
727 return 1
728
729 if item1['description'].lower() < item2['description'].lower():
730 return -1
731
732 if item1['description'].lower() > item2['description'].lower():
733 return 1
734
735 return 0
736
737
738 if isinstance(item1, type({})):
739 return -1
740
741 return 0
742
743 from Gnumed.wxGladeWidgets import wxgScrolledEMRTreePnl
744
762
763 from Gnumed.wxGladeWidgets import wxgSplittedEMRTreeBrowserPnl
764
766 """A splitter window holding an EMR tree.
767
768 The left hand side displays a scrollable EMR tree while
769 on the right details for selected items are displayed.
770
771 Expects to be put into a Notebook.
772 """
777
779 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
780 return True
781
783 if self.GetParent().GetCurrentPage() == self:
784 self.repopulate_ui()
785 return True
786
788 """Fills UI with data."""
789 self._pnl_emr_tree.repopulate_ui()
790 self._splitter_browser.SetSashPosition(self._splitter_browser.GetSizeTuple()[0]/3, True)
791 return True
792
795 wx.Panel.__init__(self, *args, **kwargs)
796
797 self.__do_layout()
798 self.__register_events()
799
801 self.__journal = wx.TextCtrl (
802 self,
803 -1,
804 _('No EMR data loaded.'),
805 style = wx.TE_MULTILINE | wx.TE_READONLY
806 )
807 self.__journal.SetFont(wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL))
808
809 szr_outer = wx.BoxSizer(wx.VERTICAL)
810 szr_outer.Add(self.__journal, 1, wx.EXPAND, 0)
811
812 self.SetAutoLayout(1)
813 self.SetSizer(szr_outer)
814 szr_outer.Fit(self)
815 szr_outer.SetSizeHints(self)
816 self.Layout()
817
819 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
820
822 """Expects to be in a Notebook."""
823 if self.GetParent().GetCurrentPage() == self:
824 self.repopulate_ui()
825 return True
826
827
828
830 txt = StringIO.StringIO()
831 exporter = gmPatientExporter.cEMRJournalExporter()
832
833
834 try:
835 exporter.export(txt)
836 self.__journal.SetValue(txt.getvalue())
837 except ValueError:
838 _log.exception('cannot get EMR journal')
839 self.__journal.SetValue (_(
840 'An error occurred while retrieving the EMR\n'
841 'in journal form for the active patient.\n\n'
842 'Please check the log file for details.'
843 ))
844 txt.close()
845 self.__journal.ShowPosition(self.__journal.GetLastPosition())
846 return True
847
848
849
850 if __name__ == '__main__':
851
852 _log.info("starting emr browser...")
853
854 try:
855
856 patient = gmPerson.ask_for_patient()
857 if patient is None:
858 print "No patient. Exiting gracefully..."
859 sys.exit(0)
860 gmPatSearchWidgets.set_active_patient(patient = patient)
861
862
863 application = wx.PyWidgetTester(size=(800,600))
864 emr_browser = cEMRBrowserPanel(application.frame, -1)
865 emr_browser.refresh_tree()
866
867 application.frame.Show(True)
868 application.MainLoop()
869
870
871 if patient is not None:
872 try:
873 patient.cleanup()
874 except:
875 print "error cleaning up patient"
876 except StandardError:
877 _log.exception("unhandled exception caught !")
878
879 raise
880
881 _log.info("closing emr browser...")
882
883
884