1 """GNUmed patient EMR tree browser."""
2
3 __author__ = "cfmoro1976@yahoo.es, sjtan@swiftdsl.com.au, Karsten.Hilbert@gmx.net"
4 __license__ = "GPL v2 or later"
5
6
7 import sys
8 import os.path
9 import StringIO
10 import codecs
11 import logging
12
13
14
15 import wx
16
17
18
19 from Gnumed.pycommon import gmI18N
20 from Gnumed.pycommon import gmDispatcher
21 from Gnumed.pycommon import gmExceptions
22 from Gnumed.pycommon import gmTools
23 from Gnumed.pycommon import gmDateTime
24 from Gnumed.pycommon import gmLog2
25
26 from Gnumed.exporters import gmPatientExporter
27
28 from Gnumed.business import gmEMRStructItems
29 from Gnumed.business import gmPerson
30 from Gnumed.business import gmSOAPimporter
31 from Gnumed.business import gmPersonSearch
32
33 from Gnumed.wxpython import gmGuiHelpers
34 from Gnumed.wxpython import gmEMRStructWidgets
35 from Gnumed.wxpython import gmSOAPWidgets
36 from Gnumed.wxpython import gmAllergyWidgets
37 from Gnumed.wxpython import gmDemographicsWidgets
38 from Gnumed.wxpython import gmNarrativeWidgets
39 from Gnumed.wxpython import gmPatSearchWidgets
40 from Gnumed.wxpython import gmVaccWidgets
41 from Gnumed.wxpython import gmFamilyHistoryWidgets
42
43
44 _log = logging.getLogger('gm.ui')
45
46
48 """
49 Dump the patient's EMR from GUI client
50 @param parent - The parent widget
51 @type parent - A wx.Window instance
52 """
53
54 if parent is None:
55 raise TypeError('expected wx.Window instance as parent, got <None>')
56
57 pat = gmPerson.gmCurrentPatient()
58 if not pat.connected:
59 gmDispatcher.send(signal='statustext', msg=_('Cannot export EMR. No active patient.'))
60 return False
61
62
63 wc = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files"))
64 defdir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed', pat['dirname'])))
65 gmTools.mkdir(defdir)
66 fname = '%s-%s_%s.txt' % (_('emr-export'), pat['lastnames'], pat['firstnames'])
67 dlg = wx.FileDialog (
68 parent = parent,
69 message = _("Save patient's EMR as..."),
70 defaultDir = defdir,
71 defaultFile = fname,
72 wildcard = wc,
73 style = wx.SAVE
74 )
75 choice = dlg.ShowModal()
76 fname = dlg.GetPath()
77 dlg.Destroy()
78 if choice != wx.ID_OK:
79 return None
80
81 _log.debug('exporting EMR to [%s]', fname)
82
83 output_file = codecs.open(fname, 'wb', encoding='utf8', errors='replace')
84 exporter = gmPatientExporter.cEmrExport(patient = pat)
85 exporter.set_output_file(output_file)
86 exporter.dump_constraints()
87 exporter.dump_demographic_record(True)
88 exporter.dump_clinical_record()
89 exporter.dump_med_docs()
90 output_file.close()
91
92 gmDispatcher.send('statustext', msg = _('EMR successfully exported to file: %s') % fname, beep = False)
93 return fname
94
95 -class cEMRTree(wx.TreeCtrl, gmGuiHelpers.cTreeExpansionHistoryMixin):
96 """This wx.TreeCtrl derivative displays a tree view of the medical record."""
97
98
99 - def __init__(self, parent, id, *args, **kwds):
100 """Set up our specialised tree.
101 """
102 kwds['style'] = wx.TR_HAS_BUTTONS | wx.NO_BORDER | wx.TR_SINGLE
103 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds)
104
105 gmGuiHelpers.cTreeExpansionHistoryMixin.__init__(self)
106
107 self.__soap_display = None
108 self.__soap_display_mode = u'details'
109 self.__img_display = None
110 self.__cb__enable_display_mode_selection = lambda x:x
111 self.__cb__select_edit_mode = lambda x:x
112 self.__cb__add_soap_editor = lambda x:x
113 self.__pat = gmPerson.gmCurrentPatient()
114 self.__curr_node = None
115
116 self._old_cursor_pos = None
117
118 self.__make_popup_menus()
119 self.__register_events()
120
121
122
124 if not self.__pat.connected:
125 gmDispatcher.send(signal='statustext', msg=_('Cannot load clinical narrative. No active patient.'),)
126 return False
127
128 if not self.__populate_tree():
129 return False
130
131 return True
132
134 return self.__soap_display
135
138
139 soap_display = property(_get_soap_display, _set_soap_display)
140
142 return self.__img_display
143
146
147 image_display = property(_get_image_display, _set_image_display)
148
150 if not callable(callback):
151 raise ValueError('callback [%s] not callable' % callback)
152 self.__cb__enable_display_mode_selection = callback
153
155 if callback is None:
156 callback = lambda x:x
157 if not callable(callback):
158 raise ValueError('edit mode selector [%s] not callable' % callback)
159 self.__cb__select_edit_mode = callback
160
161 edit_mode_selector = property(lambda x:x, _set_edit_mode_selector)
162
164 if callback is None:
165 callback = lambda x:x
166 if not callable(callback):
167 raise ValueError('soap editor adder [%s] not callable' % callback)
168 self.__cb__add_soap_editor = callback
169
170 soap_editor_adder = property(lambda x:x, _set_soap_editor_adder)
171
172
173
174
176 """Configures enabled event signals."""
177 wx.EVT_TREE_SEL_CHANGED (self, self.GetId(), self._on_tree_item_selected)
178 wx.EVT_TREE_ITEM_RIGHT_CLICK (self, self.GetId(), self._on_tree_item_right_clicked)
179 self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self._on_tree_item_expanding)
180
181
182
183 wx.EVT_TREE_ITEM_GETTOOLTIP(self, -1, self._on_tree_item_gettooltip)
184
185 gmDispatcher.connect(signal = 'narrative_mod_db', receiver = self._on_narrative_mod_db)
186 gmDispatcher.connect(signal = 'episode_mod_db', receiver = self._on_episode_mod_db)
187 gmDispatcher.connect(signal = 'health_issue_mod_db', receiver = self._on_issue_mod_db)
188 gmDispatcher.connect(signal = 'family_history_mod_db', receiver = self._on_issue_mod_db)
189
191 self.DeleteAllItems()
192
194 """Updates EMR browser data."""
195
196
197
198 if not self.__pat.connected:
199 return
200
201 wx.BeginBusyCursor()
202
203
204 root_item = self.__populate_root_node()
205
206 self.__curr_node = root_item
207 self.SelectItem(root_item)
208 self.Expand(root_item)
209 self.__update_text_for_selected_node()
210
211 wx.EndBusyCursor()
212 return True
213
215
216 self.DeleteAllItems()
217
218 root_item = self.AddRoot(_('EMR of %(lastnames)s, %(firstnames)s') % self.__pat.get_active_name())
219 self.SetItemPyData(root_item, None)
220 self.SetItemHasChildren(root_item, True)
221
222 self.__root_tooltip = self.__pat['description_gender'] + u'\n'
223 if self.__pat['deceased'] is None:
224 self.__root_tooltip += u' %s (%s)\n\n' % (
225 self.__pat.get_formatted_dob(format = '%d %b %Y', encoding = gmI18N.get_encoding()),
226 self.__pat['medical_age']
227 )
228 else:
229 template = u' %s - %s (%s)\n\n'
230 self.__root_tooltip += template % (
231 self.__pat.get_formatted_dob(format = '%d.%b %Y', encoding = gmI18N.get_encoding()),
232 gmDateTime.pydt_strftime(self.__pat['deceased'], '%Y %b %d'),
233 self.__pat['medical_age']
234 )
235 self.__root_tooltip += gmTools.coalesce(self.__pat['comment'], u'', u'%s\n\n')
236 doc = self.__pat.primary_provider
237 if doc is not None:
238 self.__root_tooltip += u'%s:\n' % _('Primary provider in this praxis')
239 self.__root_tooltip += u' %s %s %s (%s)%s\n\n' % (
240 gmTools.coalesce(doc['title'], gmPerson.map_gender2salutation(gender = doc['gender'])),
241 doc['firstnames'],
242 doc['lastnames'],
243 doc['short_alias'],
244 gmTools.bool2subst(doc['is_active'], u'', u' [%s]' % _('inactive'))
245 )
246 if not ((self.__pat['emergency_contact'] is None) and (self.__pat['pk_emergency_contact'] is None)):
247 self.__root_tooltip += _('In case of emergency contact:') + u'\n'
248 if self.__pat['emergency_contact'] is not None:
249 self.__root_tooltip += gmTools.wrap (
250 text = u'%s\n' % self.__pat['emergency_contact'],
251 width = 60,
252 initial_indent = u' ',
253 subsequent_indent = u' '
254 )
255 if self.__pat['pk_emergency_contact'] is not None:
256 contact = self.__pat.emergency_contact_in_database
257 self.__root_tooltip += u' %s\n' % contact['description_gender']
258 self.__root_tooltip = self.__root_tooltip.strip('\n')
259 if self.__root_tooltip == u'':
260 self.__root_tooltip = u' '
261
262 return root_item
263
265 """Displays information for the selected tree node."""
266
267 if self.__soap_display is None:
268 self.__img_display.clear()
269 return
270
271 if self.__curr_node is None:
272 self.__img_display.clear()
273 return
274
275 if not self.__curr_node.IsOk():
276 return
277
278 try:
279 node_data = self.GetPyData(self.__curr_node)
280 except wx.PyAssertionError:
281 node_data = None
282 _log.exception('unfathomable self.GetPyData() problem occurred, faking root node')
283 _log.debug('real node: %s', self.__curr_node)
284 _log.debug('node.IsOk(): %s', self.__curr_node.IsOk())
285 _log.debug('is root node: %s', self.__curr_node == self.GetRootItem())
286 _log.debug('node.m_pItem: %s', getattr(self.__curr_node.m_pItem), '<NO SUCH ATTRIBUTE>')
287 _log.debug('node attributes: %s', dir(self.__curr_node))
288 gmLog2.log_stack_trace()
289 doc_folder = self.__pat.get_document_folder()
290
291 if isinstance(node_data, gmEMRStructItems.cHealthIssue):
292 self.__cb__enable_display_mode_selection(True)
293 if self.__soap_display_mode == u'details':
294 txt = node_data.format(left_margin=1, patient = self.__pat)
295 else:
296 txt = node_data.format_as_journal(left_margin = 1)
297
298 self.__img_display.refresh (
299 document_folder = doc_folder,
300 episodes = [ epi['pk_episode'] for epi in node_data.episodes ]
301 )
302
303 elif isinstance(node_data, type({})):
304 self.__cb__enable_display_mode_selection(False)
305
306 txt = _('Pool of unassociated episodes:\n\n "%s"') % node_data['description']
307 self.__img_display.clear()
308
309 elif isinstance(node_data, gmEMRStructItems.cEpisode):
310 self.__cb__enable_display_mode_selection(True)
311 if self.__soap_display_mode == u'details':
312 txt = node_data.format(left_margin = 1, patient = self.__pat)
313 else:
314 txt = node_data.format_as_journal(left_margin = 1)
315 self.__img_display.refresh (
316 document_folder = doc_folder,
317 episodes = [node_data['pk_episode']]
318 )
319
320 elif isinstance(node_data, gmEMRStructItems.cEncounter):
321 self.__cb__enable_display_mode_selection(False)
322 epi = self.GetPyData(self.GetItemParent(self.__curr_node))
323 txt = node_data.format (
324 episodes = [epi['pk_episode']],
325 with_soap = True,
326 left_margin = 1,
327 patient = self.__pat,
328 with_co_encountlet_hints = True
329 )
330 self.__img_display.refresh (
331 document_folder = doc_folder,
332 episodes = [epi['pk_episode']],
333 encounter = node_data['pk_encounter']
334 )
335
336
337 else:
338 self.__cb__enable_display_mode_selection(True)
339 if self.__soap_display_mode == u'details':
340 emr = self.__pat.get_emr()
341 txt = emr.format_summary(dob = self.__pat['dob'])
342 else:
343 txt = self.__pat.emr.format_as_journal(left_margin = 1, patient = self.__pat)
344 self.__img_display.clear()
345
346 self.__soap_display.Clear()
347 self.__soap_display.WriteText(txt)
348 self.__soap_display.ShowPosition(0)
349
351
352
353 self.__root_context_popup = wx.Menu(title = _('EMR Actions:'))
354
355 menu_id = wx.NewId()
356 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Create health issue')))
357 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__create_issue)
358
359 item = self.__root_context_popup.Append(-1, _('Create episode'))
360 self.Bind(wx.EVT_MENU, self.__create_episode, item)
361
362 item = self.__root_context_popup.Append(-1, _('Create progress note'))
363 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item)
364
365 menu_id = wx.NewId()
366 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage allergies')))
367 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__document_allergy)
368
369 menu_id = wx.NewId()
370 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage family history')))
371 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_family_history)
372
373 menu_id = wx.NewId()
374 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage hospitalizations')))
375 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_hospital_stays)
376
377 menu_id = wx.NewId()
378 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage occupation')))
379 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_occupation)
380
381 menu_id = wx.NewId()
382 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage procedures')))
383 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_procedures)
384
385 menu_id = wx.NewId()
386 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage vaccinations')))
387 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_vaccinations)
388
389 self.__root_context_popup.AppendSeparator()
390
391
392 expand_menu = wx.Menu()
393 self.__root_context_popup.AppendMenu(wx.NewId(), _('Open EMR to ...'), expand_menu)
394
395 menu_id = wx.NewId()
396 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... issue level')))
397 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_issue_level)
398
399 menu_id = wx.NewId()
400 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... episode level')))
401 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_episode_level)
402
403 menu_id = wx.NewId()
404 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... encounter level')))
405 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_encounter_level)
406
407
408 self.__issue_context_popup = wx.Menu(title = _('Health Issue Actions:'))
409
410 menu_id = wx.NewId()
411 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Edit details')))
412 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__edit_issue)
413
414 menu_id = wx.NewId()
415 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Delete')))
416 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__delete_issue)
417
418 self.__issue_context_popup.AppendSeparator()
419
420 menu_id = wx.NewId()
421 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Open to encounter level')))
422 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__expand_issue_to_encounter_level)
423
424
425
426 item = self.__issue_context_popup.Append(-1, _('Create progress note'))
427 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item)
428
429
430 self.__epi_context_popup = wx.Menu(title = _('Episode Actions:'))
431
432 menu_id = wx.NewId()
433 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Edit details')))
434 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__edit_episode)
435
436 menu_id = wx.NewId()
437 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Delete')))
438 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__delete_episode)
439
440 menu_id = wx.NewId()
441 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Promote')))
442 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__promote_episode_to_issue)
443
444 item = self.__epi_context_popup.Append(-1, _('Create progress note'))
445 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item)
446
447 menu_id = wx.NewId()
448 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Move encounters')))
449 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__move_encounters)
450
451
452 self.__enc_context_popup = wx.Menu(title = _('Encounter Actions:'))
453
454 menu_id = wx.NewId()
455 self.__enc_context_popup.AppendItem(wx.MenuItem(self.__enc_context_popup, menu_id, _('Move data to another episode')))
456 wx.EVT_MENU(self.__enc_context_popup, menu_id, self.__relink_encounter_data2episode)
457
458 menu_id = wx.NewId()
459 self.__enc_context_popup.AppendItem(wx.MenuItem(self.__enc_context_popup, menu_id, _('Edit details')))
460 wx.EVT_MENU(self.__enc_context_popup, menu_id, self.__edit_encounter_details)
461
462
463
464
465
466 item = self.__enc_context_popup.Append(-1, _('Edit progress notes'))
467 self.Bind(wx.EVT_MENU, self.__edit_progress_notes, item)
468
469 item = self.__enc_context_popup.Append(-1, _('Move progress notes'))
470 self.Bind(wx.EVT_MENU, self.__move_progress_notes, item)
471
472 item = self.__enc_context_popup.Append(-1, _('Export for Medistar'))
473 self.Bind(wx.EVT_MENU, self.__export_encounter_for_medistar, item)
474
475 - def __handle_root_context(self, pos=wx.DefaultPosition):
476 self.PopupMenu(self.__root_context_popup, pos)
477
478 - def __handle_issue_context(self, pos=wx.DefaultPosition):
479
480 self.PopupMenu(self.__issue_context_popup, pos)
481
482 - def __handle_episode_context(self, pos=wx.DefaultPosition):
483
484 self.PopupMenu(self.__epi_context_popup, pos)
485
486 - def __handle_encounter_context(self, pos=wx.DefaultPosition):
487 self.PopupMenu(self.__enc_context_popup, pos)
488
489
490
499
502
506
508 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
509 parent = self,
510 id = -1,
511 caption = _('Deleting episode'),
512 button_defs = [
513 {'label': _('Yes, delete'), 'tooltip': _('Delete the episode if possible (it must be completely empty).')},
514 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the episode.')}
515 ],
516 question = _(
517 'Are you sure you want to delete this episode ?\n'
518 '\n'
519 ' "%s"\n'
520 ) % self.__curr_node_data['description']
521 )
522 result = dlg.ShowModal()
523 if result != wx.ID_YES:
524 return
525
526 if not gmEMRStructItems.delete_episode(episode = self.__curr_node_data):
527 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete episode. There is still clinical data recorded for it.'))
528
530 self.DeleteChildren(episode_node)
531
532 emr = self.__pat.emr
533 epi = self.GetPyData(episode_node)
534 encounters = emr.get_encounters(episodes = [epi['pk_episode']], skip_empty = True)
535 if len(encounters) == 0:
536 self.SetItemHasChildren(episode_node, False)
537 return
538
539 self.SetItemHasChildren(episode_node, True)
540
541 for enc in encounters:
542 label = u'%s: %s' % (
543 enc['started'].strftime('%Y-%m-%d'),
544 gmTools.unwrap (
545 gmTools.coalesce (
546 gmTools.coalesce (
547 gmTools.coalesce (
548 enc.get_latest_soap (
549 soap_cat = 'a',
550 episode = epi['pk_episode']
551 ),
552 enc['assessment_of_encounter']
553 ),
554 enc['reason_for_encounter']
555 ),
556 enc['l10n_type']
557 ),
558 max_length = 40
559 )
560 )
561 encounter_node = self.AppendItem(episode_node, label)
562 self.SetItemPyData(encounter_node, enc)
563
564 self.SetItemHasChildren(encounter_node, False)
565
566 self.SortChildren(episode_node)
567
568
569
580
582 encounter = self.GetPyData(self.__curr_node)
583 node_parent = self.GetItemParent(self.__curr_node)
584 episode = self.GetPyData(node_parent)
585
586 gmNarrativeWidgets.manage_progress_notes (
587 parent = self,
588 encounters = [encounter['pk_encounter']],
589 episodes = [episode['pk_episode']]
590 )
591
596
598
599 node_parent = self.GetItemParent(self.__curr_node)
600 owning_episode = self.GetPyData(node_parent)
601
602 episode_selector = gmNarrativeWidgets.cMoveNarrativeDlg (
603 self,
604 -1,
605 episode = owning_episode,
606 encounter = self.__curr_node_data
607 )
608
609 result = episode_selector.ShowModal()
610 episode_selector.Destroy()
611
612 if result == wx.ID_YES:
613 self.__populate_tree()
614
615
616
619
621 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
622 parent = self,
623 id = -1,
624 caption = _('Deleting health issue'),
625 button_defs = [
626 {'label': _('Yes, delete'), 'tooltip': _('Delete the health issue if possible (it must be completely empty).')},
627 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the health issue.')}
628 ],
629 question = _(
630 'Are you sure you want to delete this health issue ?\n'
631 '\n'
632 ' "%s"\n'
633 ) % self.__curr_node_data['description']
634 )
635 result = dlg.ShowModal()
636 if result != wx.ID_YES:
637 dlg.Destroy()
638 return
639
640 dlg.Destroy()
641
642 try:
643 gmEMRStructItems.delete_health_issue(health_issue = self.__curr_node_data)
644 except gmExceptions.DatabaseObjectInUseError:
645 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete health issue. There is still clinical data recorded for it.'))
646
648
649 if not self.__curr_node.IsOk():
650 return
651
652 self.Expand(self.__curr_node)
653
654 epi, epi_cookie = self.GetFirstChild(self.__curr_node)
655 while epi.IsOk():
656 self.Expand(epi)
657 epi, epi_cookie = self.GetNextChild(self.__curr_node, epi_cookie)
658
660 self.DeleteChildren(issue_node)
661
662 emr = self.__pat.emr
663 issue = self.GetPyData(issue_node)
664 episodes = emr.get_episodes(issues = [issue['pk_health_issue']])
665 if len(episodes) == 0:
666 self.SetItemHasChildren(issue_node, False)
667 return
668
669 self.SetItemHasChildren(issue_node, True)
670
671 for episode in episodes:
672 episode_node = self.AppendItem(issue_node, episode['description'])
673 self.SetItemPyData(episode_node, episode)
674
675 self.SetItemHasChildren(episode_node, True)
676
677 self.SortChildren(issue_node)
678
680 self.DeleteChildren(fake_issue_node)
681
682 emr = self.__pat.emr
683 episodes = emr.unlinked_episodes
684 if len(episodes) == 0:
685 self.SetItemHasChildren(fake_issue_node, False)
686 return
687
688 self.SetItemHasChildren(fake_issue_node, True)
689
690 for episode in episodes:
691 episode_node = self.AppendItem(fake_issue_node, episode['description'])
692 self.SetItemPyData(episode_node, episode)
693 if episode['episode_open']:
694 self.SetItemBold(fake_issue_node, True)
695
696 self.SetItemHasChildren(episode_node, True)
697
698 self.SortChildren(fake_issue_node)
699
700
701
704
707
709 self.__cb__select_edit_mode(True)
710 self.__cb__add_soap_editor(problem = self.__curr_node_data, allow_same_problem = False)
711
719
722
725
728
731
734
736
737 root_item = self.GetRootItem()
738
739 if not root_item.IsOk():
740 return
741
742 self.Expand(root_item)
743
744
745 issue, issue_cookie = self.GetFirstChild(root_item)
746 while issue.IsOk():
747 self.Collapse(issue)
748 epi, epi_cookie = self.GetFirstChild(issue)
749 while epi.IsOk():
750 self.Collapse(epi)
751 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
752 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
753
755
756 root_item = self.GetRootItem()
757
758 if not root_item.IsOk():
759 return
760
761 self.Expand(root_item)
762
763
764 issue, issue_cookie = self.GetFirstChild(root_item)
765 while issue.IsOk():
766 self.Expand(issue)
767 epi, epi_cookie = self.GetFirstChild(issue)
768 while epi.IsOk():
769 self.Collapse(epi)
770 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
771 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
772
774
775 root_item = self.GetRootItem()
776
777 if not root_item.IsOk():
778 return
779
780 self.Expand(root_item)
781
782
783 issue, issue_cookie = self.GetFirstChild(root_item)
784 while issue.IsOk():
785 self.Expand(issue)
786 epi, epi_cookie = self.GetFirstChild(issue)
787 while epi.IsOk():
788 self.Expand(epi)
789 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
790 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
791
798
800 root_node = self.GetRootItem()
801 self.DeleteChildren(root_node)
802
803 issues = [{
804 'description': _('Unattributed episodes'),
805 'has_open_episode': False,
806 'pk_health_issue': None
807 }]
808
809 emr = self.__pat.emr
810 issues.extend(emr.health_issues)
811
812 for issue in issues:
813 issue_node = self.AppendItem(root_node, issue['description'])
814 self.SetItemBold(issue_node, issue['has_open_episode'])
815 self.SetItemPyData(issue_node, issue)
816
817 self.SetItemHasChildren(issue_node, True)
818
819 self.SetItemHasChildren(root_node, (len(issues) != 0))
820 self.SortChildren(root_node)
821
822
823
825 wx.CallAfter(self.__update_text_for_selected_node)
826
828 wx.CallAfter(self.__populate_tree)
829
831 wx.CallAfter(self.__populate_tree)
832
834 if not self.__pat.connected:
835 return
836
837 event.Skip()
838
839 node = event.GetItem()
840 if node == self.GetRootItem():
841 self.__expand_root_node()
842 return
843
844 node_data = self.GetPyData(node)
845
846 if isinstance(node_data, gmEMRStructItems.cHealthIssue):
847 self.__expand_issue_node(issue_node = node)
848 return
849
850 if isinstance(node_data, gmEMRStructItems.cEpisode):
851 self.__expand_episode_node(episode_node = node)
852 return
853
854
855 if type(node_data) == type({}):
856 self.__expand_pseudo_issue_node(fake_issue_node = node)
857 return
858
859
860
861
863 sel_item = event.GetItem()
864 self.__curr_node = sel_item
865 self.__update_text_for_selected_node()
866 return True
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1018 """Right button clicked: display the popup for the tree"""
1019
1020 node = event.GetItem()
1021 self.SelectItem(node)
1022 self.__curr_node_data = self.GetPyData(node)
1023 self.__curr_node = node
1024
1025 pos = wx.DefaultPosition
1026 if isinstance(self.__curr_node_data, gmEMRStructItems.cHealthIssue):
1027 self.__handle_issue_context(pos=pos)
1028 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEpisode):
1029 self.__handle_episode_context(pos=pos)
1030 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEncounter):
1031 self.__handle_encounter_context(pos=pos)
1032 elif node == self.GetRootItem():
1033 self.__handle_root_context()
1034 elif type(self.__curr_node_data) == type({}):
1035
1036 pass
1037 else:
1038 print "error: unknown node type, no popup menu"
1039 event.Skip()
1040
1042 """Used in sorting items.
1043
1044 -1: 1 < 2
1045 0: 1 = 2
1046 1: 1 > 2
1047 """
1048
1049
1050 if not node1:
1051 _log.debug('invalid node 1')
1052 return 0
1053 if not node2:
1054 _log.debug('invalid node 2')
1055 return 0
1056
1057 if not node1.IsOk():
1058 _log.debug('invalid node 1')
1059 return 0
1060 if not node2.IsOk():
1061 _log.debug('invalid node 2')
1062 return 0
1063
1064 item1 = self.GetPyData(node1)
1065 item2 = self.GetPyData(node2)
1066
1067
1068 if isinstance(item1, type({})):
1069 return -1
1070 if isinstance(item2, type({})):
1071 return 1
1072
1073
1074 if isinstance(item1, gmEMRStructItems.cEncounter):
1075 if item1['started'] == item2['started']:
1076 return 0
1077 if item1['started'] > item2['started']:
1078 return -1
1079 return 1
1080
1081
1082 if isinstance(item1, gmEMRStructItems.cEpisode):
1083 start1 = item1.best_guess_start_date
1084 start2 = item2.best_guess_start_date
1085 if start1 == start2:
1086 return 0
1087 if start1 < start2:
1088 return -1
1089 return 1
1090
1091
1092 if isinstance(item1, gmEMRStructItems.cHealthIssue):
1093
1094
1095 if item1['grouping'] is None:
1096 if item2['grouping'] is not None:
1097 return 1
1098
1099
1100 if item1['grouping'] is not None:
1101 if item2['grouping'] is None:
1102 return -1
1103
1104
1105 if (item1['grouping'] is None) and (item2['grouping'] is None):
1106 if item1['description'].lower() < item2['description'].lower():
1107 return -1
1108 if item1['description'].lower() > item2['description'].lower():
1109 return 1
1110 return 0
1111
1112
1113 if item1['grouping'] < item2['grouping']:
1114 return -1
1115
1116 if item1['grouping'] > item2['grouping']:
1117 return 1
1118
1119 if item1['description'].lower() < item2['description'].lower():
1120 return -1
1121
1122 if item1['description'].lower() > item2['description'].lower():
1123 return 1
1124
1125 return 0
1126
1127 _log.error('unknown item type during sorting EMR tree:')
1128 _log.error('item1: %s', type(item1))
1129 _log.error('item2: %s', type(item2))
1130
1131 return 0
1132
1133
1134
1136 return self.__soap_display_mode
1137
1139 if mode not in [u'details', u'journal']:
1140 raise ValueError('details display mode must be one of "details", "journal"')
1141 if self.__soap_display_mode == mode:
1142 return
1143 self.__soap_display_mode = mode
1144 self.__update_text_for_selected_node()
1145
1146 details_display_mode = property(_get_details_display_mode, _set_details_display_mode)
1147
1148 from Gnumed.wxGladeWidgets import wxgScrolledEMRTreePnl
1149
1163
1164 from Gnumed.wxGladeWidgets import wxgSplittedEMRTreeBrowserPnl
1165
1167 """A splitter window holding an EMR tree.
1168
1169 The left hand side displays a scrollable EMR tree while
1170 on the right details for selected items are displayed.
1171
1172 Expects to be put into a Notebook.
1173 """
1184
1186 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1187 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1188 return True
1189
1191 return self.__editing
1192
1194 self.__editing = editing
1195 self.enable_display_mode_selection(enable = not self.__editing)
1196 if self.__editing:
1197 self._BTN_switch_browse_edit.SetLabel(_('&Browse'))
1198 self._PNL_browse.Hide()
1199 self._PNL_visual_soap.Hide()
1200 self._PNL_edit.Show()
1201 else:
1202 self._BTN_switch_browse_edit.SetLabel(_('&Edit'))
1203 self._PNL_edit.Hide()
1204 self._PNL_visual_soap.Show()
1205 self._PNL_browse.Show()
1206 self._PNL_right_side.GetSizer().Layout()
1207
1208 editing = property(_get_editing, _set_editing)
1209
1210
1211
1213 self._pnl_emr_tree._emr_tree.clear_tree()
1214 self._PNL_edit.patient = None
1215 return True
1216
1218 wx.CallAfter(self.__on_post_patient_selection)
1219 return True
1220
1222 if self.GetParent().GetCurrentPage() != self:
1223 return True
1224 self.repopulate_ui()
1225
1228
1231
1234
1235
1236
1238 """Fills UI with data."""
1239 self._pnl_emr_tree.repopulate_ui()
1240 self._PNL_edit.patient = gmPerson.gmCurrentPatient()
1241 self._splitter_browser.SetSashPosition(self._splitter_browser.GetSizeTuple()[0]/3, True)
1242 return True
1243
1245 if self.editing:
1246 enable = False
1247 if enable:
1248 self._RBTN_details.Enable(True)
1249 self._RBTN_journal.Enable(True)
1250 return
1251 self._RBTN_details.Enable(False)
1252 self._RBTN_journal.Enable(False)
1253
1255 self._PNL_edit._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_same_problem)
1256
1259
1260
1261 from Gnumed.wxGladeWidgets import wxgEMRJournalPluginPnl
1262
1264
1269
1270
1271
1273 self._TCTRL_journal.SetValue(u'')
1274 exporter = gmPatientExporter.cEMRJournalExporter()
1275 if self._RBTN_by_encounter.GetValue():
1276 txt = StringIO.StringIO()
1277
1278
1279 try:
1280 exporter.export(txt)
1281 self._TCTRL_journal.SetValue(txt.getvalue())
1282 except ValueError:
1283 _log.exception('cannot get EMR journal')
1284 self._TCTRL_journal.SetValue (_(
1285 'An error occurred while retrieving the EMR\n'
1286 'in journal form for the active patient.\n\n'
1287 'Please check the log file for details.'
1288 ))
1289 txt.close()
1290 else:
1291 fname = exporter.export_to_file_by_mod_time()
1292 f = codecs.open(filename = fname, mode = 'rU', encoding = 'utf8', errors = 'replace')
1293 for line in f:
1294 self._TCTRL_journal.AppendText(line)
1295 f.close()
1296
1297 self._TCTRL_journal.ShowPosition(self._TCTRL_journal.GetLastPosition())
1298 return True
1299
1300
1301
1303 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1304 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1305 return True
1306
1307
1308
1310 self._TCTRL_journal.SetValue(u'')
1311 return True
1312
1314 wx.CallAfter(self.__on_post_patient_selection)
1315 return True
1316
1318 if self.GetParent().GetCurrentPage() != self:
1319 return True
1320 self.repopulate_ui()
1321
1324
1327
1328
1331 wx.Panel.__init__(self, *args, **kwargs)
1332
1333 self.__do_layout()
1334 self.__register_events()
1335
1337 self.__journal = wx.TextCtrl (
1338 self,
1339 -1,
1340 _('No EMR data loaded.'),
1341 style = wx.TE_MULTILINE | wx.TE_READONLY
1342 )
1343 self.__journal.SetFont(wx.Font(10, wx.MODERN, wx.NORMAL, wx.NORMAL))
1344
1345 szr_outer = wx.BoxSizer(wx.VERTICAL)
1346 szr_outer.Add(self.__journal, 1, wx.EXPAND, 0)
1347
1348 self.SetAutoLayout(1)
1349 self.SetSizer(szr_outer)
1350 szr_outer.Fit(self)
1351 szr_outer.SetSizeHints(self)
1352 self.Layout()
1353
1355 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1356
1358 """Expects to be in a Notebook."""
1359 if self.GetParent().GetCurrentPage() == self:
1360 self.repopulate_ui()
1361 return True
1362
1363
1364
1366
1367 exporter = gmPatientExporter.cEMRJournalExporter()
1368 fname = exporter.export_to_file_by_mod_time()
1369 f = codecs.open(filename = fname, mode = 'rU', encoding = 'utf8', errors = 'replace')
1370 for line in f:
1371 self.__journal.AppendText(line)
1372 f.close()
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388 self.__journal.ShowPosition(self.__journal.GetLastPosition())
1389 return True
1390
1391
1392
1393 if __name__ == '__main__':
1394
1395 _log.info("starting emr browser...")
1396
1397 try:
1398
1399 patient = gmPersonSearch.ask_for_patient()
1400 if patient is None:
1401 print "No patient. Exiting gracefully..."
1402 sys.exit(0)
1403 gmPatSearchWidgets.set_active_patient(patient = patient)
1404
1405
1406 application = wx.PyWidgetTester(size=(800,600))
1407 emr_browser = cEMRBrowserPanel(application.frame, -1)
1408 emr_browser.refresh_tree()
1409
1410 application.frame.Show(True)
1411 application.MainLoop()
1412
1413
1414 if patient is not None:
1415 try:
1416 patient.cleanup()
1417 except:
1418 print "error cleaning up patient"
1419 except StandardError:
1420 _log.exception("unhandled exception caught !")
1421
1422 raise
1423
1424 _log.info("closing emr browser...")
1425
1426
1427