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
186 gmDispatcher.connect(signal = 'narrative_mod_db', receiver = self._on_narrative_mod_db)
187 gmDispatcher.connect(signal = 'clin.episode_mod_db', receiver = self._on_episode_mod_db)
188 gmDispatcher.connect(signal = 'clin.health_issue_mod_db', receiver = self._on_issue_mod_db)
189 gmDispatcher.connect(signal = 'clin.family_history_mod_db', receiver = self._on_issue_mod_db)
190
192 self.DeleteAllItems()
193
195 """Updates EMR browser data."""
196
197
198
199 if not self.__pat.connected:
200 return
201
202 wx.BeginBusyCursor()
203
204
205 root_item = self.__populate_root_node()
206
207 self.__curr_node = root_item
208 self.SelectItem(root_item)
209 self.Expand(root_item)
210 self.__update_text_for_selected_node()
211
212 wx.EndBusyCursor()
213 return True
214
216
217 self.DeleteAllItems()
218
219 root_item = self.AddRoot(_('EMR of %(lastnames)s, %(firstnames)s') % self.__pat.get_active_name())
220 self.SetItemPyData(root_item, None)
221 self.SetItemHasChildren(root_item, True)
222
223 self.__root_tooltip = self.__pat['description_gender'] + u'\n'
224 if self.__pat['deceased'] is None:
225 self.__root_tooltip += u' %s (%s)\n\n' % (
226 self.__pat.get_formatted_dob(format = '%d %b %Y', encoding = gmI18N.get_encoding()),
227 self.__pat['medical_age']
228 )
229 else:
230 template = u' %s - %s (%s)\n\n'
231 self.__root_tooltip += template % (
232 self.__pat.get_formatted_dob(format = '%d.%b %Y', encoding = gmI18N.get_encoding()),
233 gmDateTime.pydt_strftime(self.__pat['deceased'], '%Y %b %d'),
234 self.__pat['medical_age']
235 )
236 self.__root_tooltip += gmTools.coalesce(self.__pat['comment'], u'', u'%s\n\n')
237 doc = self.__pat.primary_provider
238 if doc is not None:
239 self.__root_tooltip += u'%s:\n' % _('Primary provider in this praxis')
240 self.__root_tooltip += u' %s %s %s (%s)%s\n\n' % (
241 gmTools.coalesce(doc['title'], gmPerson.map_gender2salutation(gender = doc['gender'])),
242 doc['firstnames'],
243 doc['lastnames'],
244 doc['short_alias'],
245 gmTools.bool2subst(doc['is_active'], u'', u' [%s]' % _('inactive'))
246 )
247 if not ((self.__pat['emergency_contact'] is None) and (self.__pat['pk_emergency_contact'] is None)):
248 self.__root_tooltip += _('In case of emergency contact:') + u'\n'
249 if self.__pat['emergency_contact'] is not None:
250 self.__root_tooltip += gmTools.wrap (
251 text = u'%s\n' % self.__pat['emergency_contact'],
252 width = 60,
253 initial_indent = u' ',
254 subsequent_indent = u' '
255 )
256 if self.__pat['pk_emergency_contact'] is not None:
257 contact = self.__pat.emergency_contact_in_database
258 self.__root_tooltip += u' %s\n' % contact['description_gender']
259 self.__root_tooltip = self.__root_tooltip.strip('\n')
260 if self.__root_tooltip == u'':
261 self.__root_tooltip = u' '
262
263 return root_item
264
266 """Displays information for the selected tree node."""
267
268 if self.__soap_display is None:
269 self.__img_display.clear()
270 return
271
272 if self.__curr_node is None:
273 self.__img_display.clear()
274 return
275
276 if not self.__curr_node.IsOk():
277 return
278
279 try:
280 node_data = self.GetPyData(self.__curr_node)
281 except wx.PyAssertionError:
282 node_data = None
283 _log.exception('unfathomable self.GetPyData() problem occurred, faking root node')
284 _log.error('real node: %s', self.__curr_node)
285 _log.error('node.IsOk(): %s', self.__curr_node.IsOk())
286 _log.error('is root node: %s', self.__curr_node == self.GetRootItem())
287 _log.error('node.m_pItem: %s', getattr(self.__curr_node, 'm_pItem', '<NO SUCH ATTRIBUTE>'))
288 _log.error('node attributes: %s', dir(self.__curr_node))
289 gmLog2.log_stack_trace()
290 doc_folder = self.__pat.get_document_folder()
291
292 if isinstance(node_data, gmEMRStructItems.cHealthIssue):
293 self.__cb__enable_display_mode_selection(True)
294 if self.__soap_display_mode == u'details':
295 txt = node_data.format(left_margin=1, patient = self.__pat)
296 else:
297 txt = node_data.format_as_journal(left_margin = 1)
298
299 self.__img_display.refresh (
300 document_folder = doc_folder,
301 episodes = [ epi['pk_episode'] for epi in node_data.episodes ]
302 )
303
304 elif isinstance(node_data, type({})):
305 self.__cb__enable_display_mode_selection(False)
306
307 txt = _('Pool of unassociated episodes:\n\n "%s"') % node_data['description']
308 self.__img_display.clear()
309
310 elif isinstance(node_data, gmEMRStructItems.cEpisode):
311 self.__cb__enable_display_mode_selection(True)
312 if self.__soap_display_mode == u'details':
313 txt = node_data.format(left_margin = 1, patient = self.__pat)
314 else:
315 txt = node_data.format_as_journal(left_margin = 1)
316 self.__img_display.refresh (
317 document_folder = doc_folder,
318 episodes = [node_data['pk_episode']]
319 )
320
321 elif isinstance(node_data, gmEMRStructItems.cEncounter):
322 self.__cb__enable_display_mode_selection(False)
323 epi = self.GetPyData(self.GetItemParent(self.__curr_node))
324 txt = node_data.format (
325 episodes = [epi['pk_episode']],
326 with_soap = True,
327 left_margin = 1,
328 patient = self.__pat,
329 with_co_encountlet_hints = True
330 )
331 self.__img_display.refresh (
332 document_folder = doc_folder,
333 episodes = [epi['pk_episode']],
334 encounter = node_data['pk_encounter']
335 )
336
337
338 else:
339 self.__cb__enable_display_mode_selection(True)
340 if self.__soap_display_mode == u'details':
341 emr = self.__pat.get_emr()
342 txt = emr.format_summary(dob = self.__pat['dob'])
343 else:
344 txt = self.__pat.emr.format_as_journal(left_margin = 1, patient = self.__pat)
345 self.__img_display.clear()
346
347 self.__soap_display.Clear()
348 self.__soap_display.WriteText(txt)
349 self.__soap_display.ShowPosition(0)
350
352
353
354 self.__root_context_popup = wx.Menu(title = _('EMR Actions:'))
355
356 menu_id = wx.NewId()
357 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Create health issue')))
358 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__create_issue)
359
360 item = self.__root_context_popup.Append(-1, _('Create episode'))
361 self.Bind(wx.EVT_MENU, self.__create_episode, item)
362
363 item = self.__root_context_popup.Append(-1, _('Create progress note'))
364 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item)
365
366 menu_id = wx.NewId()
367 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage allergies')))
368 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__document_allergy)
369
370 menu_id = wx.NewId()
371 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage family history')))
372 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_family_history)
373
374 menu_id = wx.NewId()
375 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage hospitalizations')))
376 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_hospital_stays)
377
378 menu_id = wx.NewId()
379 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage occupation')))
380 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_occupation)
381
382 menu_id = wx.NewId()
383 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage procedures')))
384 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_procedures)
385
386 menu_id = wx.NewId()
387 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage vaccinations')))
388 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_vaccinations)
389
390 self.__root_context_popup.AppendSeparator()
391
392
393 expand_menu = wx.Menu()
394 self.__root_context_popup.AppendMenu(wx.NewId(), _('Open EMR to ...'), expand_menu)
395
396 menu_id = wx.NewId()
397 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... issue level')))
398 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_issue_level)
399
400 menu_id = wx.NewId()
401 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... episode level')))
402 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_episode_level)
403
404 menu_id = wx.NewId()
405 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... encounter level')))
406 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_encounter_level)
407
408
409 self.__issue_context_popup = wx.Menu(title = _('Health Issue Actions:'))
410
411 menu_id = wx.NewId()
412 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Edit details')))
413 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__edit_issue)
414
415 menu_id = wx.NewId()
416 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Delete')))
417 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__delete_issue)
418
419 self.__issue_context_popup.AppendSeparator()
420
421 menu_id = wx.NewId()
422 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Open to encounter level')))
423 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__expand_issue_to_encounter_level)
424
425
426
427 item = self.__issue_context_popup.Append(-1, _('Create progress note'))
428 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item)
429
430
431 self.__epi_context_popup = wx.Menu(title = _('Episode Actions:'))
432
433 menu_id = wx.NewId()
434 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Edit details')))
435 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__edit_episode)
436
437 menu_id = wx.NewId()
438 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Delete')))
439 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__delete_episode)
440
441 menu_id = wx.NewId()
442 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Promote')))
443 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__promote_episode_to_issue)
444
445 item = self.__epi_context_popup.Append(-1, _('Create progress note'))
446 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item)
447
448 menu_id = wx.NewId()
449 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Move encounters')))
450 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__move_encounters)
451
452
453 self.__enc_context_popup = wx.Menu(title = _('Encounter Actions:'))
454
455 menu_id = wx.NewId()
456 self.__enc_context_popup.AppendItem(wx.MenuItem(self.__enc_context_popup, menu_id, _('Move data to another episode')))
457 wx.EVT_MENU(self.__enc_context_popup, menu_id, self.__relink_encounter_data2episode)
458
459 menu_id = wx.NewId()
460 self.__enc_context_popup.AppendItem(wx.MenuItem(self.__enc_context_popup, menu_id, _('Edit details')))
461 wx.EVT_MENU(self.__enc_context_popup, menu_id, self.__edit_encounter_details)
462
463
464
465
466
467 item = self.__enc_context_popup.Append(-1, _('Edit progress notes'))
468 self.Bind(wx.EVT_MENU, self.__edit_progress_notes, item)
469
470 item = self.__enc_context_popup.Append(-1, _('Move progress notes'))
471 self.Bind(wx.EVT_MENU, self.__move_progress_notes, item)
472
473 item = self.__enc_context_popup.Append(-1, _('Export for Medistar'))
474 self.Bind(wx.EVT_MENU, self.__export_encounter_for_medistar, item)
475
476 - def __handle_root_context(self, pos=wx.DefaultPosition):
477 self.PopupMenu(self.__root_context_popup, pos)
478
479 - def __handle_issue_context(self, pos=wx.DefaultPosition):
480
481 self.PopupMenu(self.__issue_context_popup, pos)
482
483 - def __handle_episode_context(self, pos=wx.DefaultPosition):
484
485 self.PopupMenu(self.__epi_context_popup, pos)
486
487 - def __handle_encounter_context(self, pos=wx.DefaultPosition):
488 self.PopupMenu(self.__enc_context_popup, pos)
489
490
491
500
503
507
509 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
510 parent = self,
511 id = -1,
512 caption = _('Deleting episode'),
513 button_defs = [
514 {'label': _('Yes, delete'), 'tooltip': _('Delete the episode if possible (it must be completely empty).')},
515 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the episode.')}
516 ],
517 question = _(
518 'Are you sure you want to delete this episode ?\n'
519 '\n'
520 ' "%s"\n'
521 ) % self.__curr_node_data['description']
522 )
523 result = dlg.ShowModal()
524 if result != wx.ID_YES:
525 return
526
527 if not gmEMRStructItems.delete_episode(episode = self.__curr_node_data):
528 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete episode. There is still clinical data recorded for it.'))
529
531 self.DeleteChildren(episode_node)
532
533 emr = self.__pat.emr
534 epi = self.GetPyData(episode_node)
535 encounters = emr.get_encounters(episodes = [epi['pk_episode']], skip_empty = True)
536 if len(encounters) == 0:
537 self.SetItemHasChildren(episode_node, False)
538 return
539
540 self.SetItemHasChildren(episode_node, True)
541
542 for enc in encounters:
543 label = u'%s: %s' % (
544 enc['started'].strftime('%Y-%m-%d'),
545 gmTools.unwrap (
546 gmTools.coalesce (
547 gmTools.coalesce (
548 gmTools.coalesce (
549 enc.get_latest_soap (
550 soap_cat = 'a',
551 episode = epi['pk_episode']
552 ),
553 enc['assessment_of_encounter']
554 ),
555 enc['reason_for_encounter']
556 ),
557 enc['l10n_type']
558 ),
559 max_length = 40
560 )
561 )
562 encounter_node = self.AppendItem(episode_node, label)
563 self.SetItemPyData(encounter_node, enc)
564
565 self.SetItemHasChildren(encounter_node, False)
566
567 self.SortChildren(episode_node)
568
569
570
581
583 encounter = self.GetPyData(self.__curr_node)
584 node_parent = self.GetItemParent(self.__curr_node)
585 episode = self.GetPyData(node_parent)
586
587 gmNarrativeWidgets.manage_progress_notes (
588 parent = self,
589 encounters = [encounter['pk_encounter']],
590 episodes = [episode['pk_episode']]
591 )
592
597
599
600 node_parent = self.GetItemParent(self.__curr_node)
601 owning_episode = self.GetPyData(node_parent)
602
603 episode_selector = gmNarrativeWidgets.cMoveNarrativeDlg (
604 self,
605 -1,
606 episode = owning_episode,
607 encounter = self.__curr_node_data
608 )
609
610 result = episode_selector.ShowModal()
611 episode_selector.Destroy()
612
613 if result == wx.ID_YES:
614 self.__populate_tree()
615
616
617
620
622 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
623 parent = self,
624 id = -1,
625 caption = _('Deleting health issue'),
626 button_defs = [
627 {'label': _('Yes, delete'), 'tooltip': _('Delete the health issue if possible (it must be completely empty).')},
628 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the health issue.')}
629 ],
630 question = _(
631 'Are you sure you want to delete this health issue ?\n'
632 '\n'
633 ' "%s"\n'
634 ) % self.__curr_node_data['description']
635 )
636 result = dlg.ShowModal()
637 if result != wx.ID_YES:
638 dlg.Destroy()
639 return
640
641 dlg.Destroy()
642
643 try:
644 gmEMRStructItems.delete_health_issue(health_issue = self.__curr_node_data)
645 except gmExceptions.DatabaseObjectInUseError:
646 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete health issue. There is still clinical data recorded for it.'))
647
649
650 if not self.__curr_node.IsOk():
651 return
652
653 self.Expand(self.__curr_node)
654
655 epi, epi_cookie = self.GetFirstChild(self.__curr_node)
656 while epi.IsOk():
657 self.Expand(epi)
658 epi, epi_cookie = self.GetNextChild(self.__curr_node, epi_cookie)
659
661 self.DeleteChildren(issue_node)
662
663 emr = self.__pat.emr
664 issue = self.GetPyData(issue_node)
665 episodes = emr.get_episodes(issues = [issue['pk_health_issue']])
666 if len(episodes) == 0:
667 self.SetItemHasChildren(issue_node, False)
668 return
669
670 self.SetItemHasChildren(issue_node, True)
671
672 for episode in episodes:
673 episode_node = self.AppendItem(issue_node, episode['description'])
674 self.SetItemPyData(episode_node, episode)
675
676 self.SetItemHasChildren(episode_node, True)
677
678 self.SortChildren(issue_node)
679
681 self.DeleteChildren(fake_issue_node)
682
683 emr = self.__pat.emr
684 episodes = emr.unlinked_episodes
685 if len(episodes) == 0:
686 self.SetItemHasChildren(fake_issue_node, False)
687 return
688
689 self.SetItemHasChildren(fake_issue_node, True)
690
691 for episode in episodes:
692 episode_node = self.AppendItem(fake_issue_node, episode['description'])
693 self.SetItemPyData(episode_node, episode)
694 if episode['episode_open']:
695 self.SetItemBold(fake_issue_node, True)
696
697 self.SetItemHasChildren(episode_node, True)
698
699 self.SortChildren(fake_issue_node)
700
701
702
705
708
710 self.__cb__select_edit_mode(True)
711 self.__cb__add_soap_editor(problem = self.__curr_node_data, allow_same_problem = False)
712
720
723
726
729
732
735
737
738 root_item = self.GetRootItem()
739
740 if not root_item.IsOk():
741 return
742
743 self.Expand(root_item)
744
745
746 issue, issue_cookie = self.GetFirstChild(root_item)
747 while issue.IsOk():
748 self.Collapse(issue)
749 epi, epi_cookie = self.GetFirstChild(issue)
750 while epi.IsOk():
751 self.Collapse(epi)
752 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
753 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
754
756
757 root_item = self.GetRootItem()
758
759 if not root_item.IsOk():
760 return
761
762 self.Expand(root_item)
763
764
765 issue, issue_cookie = self.GetFirstChild(root_item)
766 while issue.IsOk():
767 self.Expand(issue)
768 epi, epi_cookie = self.GetFirstChild(issue)
769 while epi.IsOk():
770 self.Collapse(epi)
771 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
772 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
773
775
776 root_item = self.GetRootItem()
777
778 if not root_item.IsOk():
779 return
780
781 self.Expand(root_item)
782
783
784 issue, issue_cookie = self.GetFirstChild(root_item)
785 while issue.IsOk():
786 self.Expand(issue)
787 epi, epi_cookie = self.GetFirstChild(issue)
788 while epi.IsOk():
789 self.Expand(epi)
790 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
791 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
792
799
801 root_node = self.GetRootItem()
802 self.DeleteChildren(root_node)
803
804 issues = [{
805 'description': _('Unattributed episodes'),
806 'has_open_episode': False,
807 'pk_health_issue': None
808 }]
809
810 emr = self.__pat.emr
811 issues.extend(emr.health_issues)
812
813 for issue in issues:
814 issue_node = self.AppendItem(root_node, issue['description'])
815 self.SetItemBold(issue_node, issue['has_open_episode'])
816 self.SetItemPyData(issue_node, issue)
817
818 self.SetItemHasChildren(issue_node, True)
819
820 self.SetItemHasChildren(root_node, (len(issues) != 0))
821 self.SortChildren(root_node)
822
823
824
826 wx.CallAfter(self.__update_text_for_selected_node)
827
829 wx.CallAfter(self.__populate_tree)
830
832 wx.CallAfter(self.__populate_tree)
833
835 if not self.__pat.connected:
836 return
837
838 event.Skip()
839
840 node = event.GetItem()
841 if node == self.GetRootItem():
842 self.__expand_root_node()
843 return
844
845 node_data = self.GetPyData(node)
846
847 if isinstance(node_data, gmEMRStructItems.cHealthIssue):
848 self.__expand_issue_node(issue_node = node)
849 return
850
851 if isinstance(node_data, gmEMRStructItems.cEpisode):
852 self.__expand_episode_node(episode_node = node)
853 return
854
855
856 if type(node_data) == type({}):
857 self.__expand_pseudo_issue_node(fake_issue_node = node)
858 return
859
860
861
862
864 sel_item = event.GetItem()
865 self.__curr_node = sel_item
866 self.__update_text_for_selected_node()
867 return True
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
893
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1019 """Right button clicked: display the popup for the tree"""
1020
1021 node = event.GetItem()
1022 self.SelectItem(node)
1023 self.__curr_node_data = self.GetPyData(node)
1024 self.__curr_node = node
1025
1026 pos = wx.DefaultPosition
1027 if isinstance(self.__curr_node_data, gmEMRStructItems.cHealthIssue):
1028 self.__handle_issue_context(pos=pos)
1029 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEpisode):
1030 self.__handle_episode_context(pos=pos)
1031 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEncounter):
1032 self.__handle_encounter_context(pos=pos)
1033 elif node == self.GetRootItem():
1034 self.__handle_root_context()
1035 elif type(self.__curr_node_data) == type({}):
1036
1037 pass
1038 else:
1039 print "error: unknown node type, no popup menu"
1040 event.Skip()
1041
1043 """Used in sorting items.
1044
1045 -1: 1 < 2
1046 0: 1 = 2
1047 1: 1 > 2
1048 """
1049
1050
1051 if not node1:
1052 _log.debug('invalid node 1')
1053 return 0
1054 if not node2:
1055 _log.debug('invalid node 2')
1056 return 0
1057
1058 if not node1.IsOk():
1059 _log.debug('invalid node 1')
1060 return 0
1061 if not node2.IsOk():
1062 _log.debug('invalid node 2')
1063 return 0
1064
1065 item1 = self.GetPyData(node1)
1066 item2 = self.GetPyData(node2)
1067
1068
1069 if isinstance(item1, type({})):
1070 return -1
1071 if isinstance(item2, type({})):
1072 return 1
1073
1074
1075 if isinstance(item1, gmEMRStructItems.cEncounter):
1076 if item1['started'] == item2['started']:
1077 return 0
1078 if item1['started'] > item2['started']:
1079 return -1
1080 return 1
1081
1082
1083 if isinstance(item1, gmEMRStructItems.cEpisode):
1084 start1 = item1.best_guess_start_date
1085 start2 = item2.best_guess_start_date
1086 if start1 == start2:
1087 return 0
1088 if start1 < start2:
1089 return -1
1090 return 1
1091
1092
1093 if isinstance(item1, gmEMRStructItems.cHealthIssue):
1094
1095
1096 if item1['grouping'] is None:
1097 if item2['grouping'] is not None:
1098 return 1
1099
1100
1101 if item1['grouping'] is not None:
1102 if item2['grouping'] is None:
1103 return -1
1104
1105
1106 if (item1['grouping'] is None) and (item2['grouping'] is None):
1107 if item1['description'].lower() < item2['description'].lower():
1108 return -1
1109 if item1['description'].lower() > item2['description'].lower():
1110 return 1
1111 return 0
1112
1113
1114 if item1['grouping'] < item2['grouping']:
1115 return -1
1116
1117 if item1['grouping'] > item2['grouping']:
1118 return 1
1119
1120 if item1['description'].lower() < item2['description'].lower():
1121 return -1
1122
1123 if item1['description'].lower() > item2['description'].lower():
1124 return 1
1125
1126 return 0
1127
1128 _log.error('unknown item type during sorting EMR tree:')
1129 _log.error('item1: %s', type(item1))
1130 _log.error('item2: %s', type(item2))
1131
1132 return 0
1133
1134
1135
1137 return self.__soap_display_mode
1138
1140 if mode not in [u'details', u'journal']:
1141 raise ValueError('details display mode must be one of "details", "journal"')
1142 if self.__soap_display_mode == mode:
1143 return
1144 self.__soap_display_mode = mode
1145 self.__update_text_for_selected_node()
1146
1147 details_display_mode = property(_get_details_display_mode, _set_details_display_mode)
1148
1149 from Gnumed.wxGladeWidgets import wxgScrolledEMRTreePnl
1150
1164
1165 from Gnumed.wxGladeWidgets import wxgSplittedEMRTreeBrowserPnl
1166
1168 """A splitter window holding an EMR tree.
1169
1170 The left hand side displays a scrollable EMR tree while
1171 on the right details for selected items are displayed.
1172
1173 Expects to be put into a Notebook.
1174 """
1185
1187 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1188 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1189 return True
1190
1192 return self.__editing
1193
1195 self.__editing = editing
1196 self.enable_display_mode_selection(enable = not self.__editing)
1197 if self.__editing:
1198 self._BTN_switch_browse_edit.SetLabel(_('&Browse'))
1199 self._PNL_browse.Hide()
1200 self._PNL_visual_soap.Hide()
1201 self._PNL_edit.Show()
1202 else:
1203 self._BTN_switch_browse_edit.SetLabel(_('&Edit'))
1204 self._PNL_edit.Hide()
1205 self._PNL_visual_soap.Show()
1206 self._PNL_browse.Show()
1207 self._PNL_right_side.GetSizer().Layout()
1208
1209 editing = property(_get_editing, _set_editing)
1210
1211
1212
1214 self._pnl_emr_tree._emr_tree.clear_tree()
1215 self._PNL_edit.patient = None
1216 return True
1217
1219 wx.CallAfter(self.__on_post_patient_selection)
1220 return True
1221
1223 if self.GetParent().GetCurrentPage() != self:
1224 return True
1225 self.repopulate_ui()
1226
1229
1232
1235
1236
1237
1239 """Fills UI with data."""
1240 self._pnl_emr_tree.repopulate_ui()
1241 self._PNL_edit.patient = gmPerson.gmCurrentPatient()
1242 self._splitter_browser.SetSashPosition(self._splitter_browser.GetSizeTuple()[0]/3, True)
1243 return True
1244
1246 if self.editing:
1247 enable = False
1248 if enable:
1249 self._RBTN_details.Enable(True)
1250 self._RBTN_journal.Enable(True)
1251 return
1252 self._RBTN_details.Enable(False)
1253 self._RBTN_journal.Enable(False)
1254
1256 self._PNL_edit._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_same_problem)
1257
1260
1261
1262 from Gnumed.wxGladeWidgets import wxgEMRJournalPluginPnl
1263
1265
1270
1271
1272
1274 self._TCTRL_journal.SetValue(u'')
1275 exporter = gmPatientExporter.cEMRJournalExporter()
1276 if self._RBTN_by_encounter.GetValue():
1277 txt = StringIO.StringIO()
1278
1279
1280 try:
1281 exporter.export(txt)
1282 self._TCTRL_journal.SetValue(txt.getvalue())
1283 except ValueError:
1284 _log.exception('cannot get EMR journal')
1285 self._TCTRL_journal.SetValue (_(
1286 'An error occurred while retrieving the EMR\n'
1287 'in journal form for the active patient.\n\n'
1288 'Please check the log file for details.'
1289 ))
1290 txt.close()
1291 else:
1292 fname = exporter.export_to_file_by_mod_time()
1293 f = codecs.open(filename = fname, mode = 'rU', encoding = 'utf8', errors = 'replace')
1294 for line in f:
1295 self._TCTRL_journal.AppendText(line)
1296 f.close()
1297
1298 self._TCTRL_journal.ShowPosition(self._TCTRL_journal.GetLastPosition())
1299 return True
1300
1301
1302
1304 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1305 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1306 return True
1307
1308
1309
1311 self._TCTRL_journal.SetValue(u'')
1312 return True
1313
1315 wx.CallAfter(self.__on_post_patient_selection)
1316 return True
1317
1319 if self.GetParent().GetCurrentPage() != self:
1320 return True
1321 self.repopulate_ui()
1322
1325
1328
1329
1330
1331
1332 if __name__ == '__main__':
1333
1334 _log.info("starting emr browser...")
1335
1336 try:
1337
1338 patient = gmPersonSearch.ask_for_patient()
1339 if patient is None:
1340 print "No patient. Exiting gracefully..."
1341 sys.exit(0)
1342 gmPatSearchWidgets.set_active_patient(patient = patient)
1343
1344
1345 application = wx.PyWidgetTester(size=(800,600))
1346 emr_browser = cEMRBrowserPanel(application.frame, -1)
1347 emr_browser.refresh_tree()
1348
1349 application.frame.Show(True)
1350 application.MainLoop()
1351
1352
1353 if patient is not None:
1354 try:
1355 patient.cleanup()
1356 except:
1357 print "error cleaning up patient"
1358 except StandardError:
1359 _log.exception("unhandled exception caught !")
1360
1361 raise
1362
1363 _log.info("closing emr browser...")
1364
1365
1366