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
305 elif isinstance(node_data, type({})):
306 self.__cb__enable_display_mode_selection(True)
307 if self.__soap_display_mode == u'details':
308 txt = _('Pool of unassociated episodes "%s":\n') % node_data['description']
309 epis = self.__pat.emr.get_episodes(unlinked_only = True, order_by = u'episode_open DESC, description')
310 if len(epis) > 0:
311 txt += u'\n'
312 for epi in epis:
313 txt += epi.format (
314 left_margin = 1,
315 patient = self.__pat,
316 with_summary = True,
317 with_codes = False,
318 with_encounters = False,
319 with_documents = False,
320 with_hospital_stays = False,
321 with_procedures = False,
322 with_family_history = False,
323 with_tests = False,
324 with_vaccinations = False,
325 with_health_issue = False
326 )
327 txt += u'\n'
328 else:
329 epis = self.__pat.emr.get_episodes(unlinked_only = True, order_by = u'episode_open DESC, description')
330 txt = u''
331 if len(epis) > 0:
332 txt += _(' Listing of unassociated episodes\n')
333 for epi in epis:
334 txt += u' %s\n' % (gmTools.u_box_horiz_4dashes * 60)
335 txt += epi.format (
336 left_margin = 1,
337 patient = self.__pat,
338 with_summary = False,
339 with_codes = False,
340 with_encounters = False,
341 with_documents = False,
342 with_hospital_stays = False,
343 with_procedures = False,
344 with_family_history = False,
345 with_tests = False,
346 with_vaccinations = False,
347 with_health_issue = False
348 )
349 txt += u'\n'
350 txt += epi.format_as_journal(left_margin = 2)
351 self.__img_display.clear()
352
353 elif isinstance(node_data, gmEMRStructItems.cEpisode):
354 self.__cb__enable_display_mode_selection(True)
355 if self.__soap_display_mode == u'details':
356 txt = node_data.format(left_margin = 1, patient = self.__pat)
357 else:
358 txt = node_data.format_as_journal(left_margin = 1)
359 self.__img_display.refresh (
360 document_folder = doc_folder,
361 episodes = [node_data['pk_episode']]
362 )
363
364 elif isinstance(node_data, gmEMRStructItems.cEncounter):
365 self.__cb__enable_display_mode_selection(False)
366 epi = self.GetPyData(self.GetItemParent(self.__curr_node))
367 txt = node_data.format (
368 episodes = [epi['pk_episode']],
369 with_soap = True,
370 left_margin = 1,
371 patient = self.__pat,
372 with_co_encountlet_hints = True
373 )
374 self.__img_display.refresh (
375 document_folder = doc_folder,
376 episodes = [epi['pk_episode']],
377 encounter = node_data['pk_encounter']
378 )
379
380
381 else:
382 self.__cb__enable_display_mode_selection(True)
383 if self.__soap_display_mode == u'details':
384 emr = self.__pat.get_emr()
385 txt = emr.format_summary()
386 else:
387 txt = self.__pat.emr.format_as_journal(left_margin = 1, patient = self.__pat)
388 self.__img_display.clear()
389
390 self.__soap_display.Clear()
391 self.__soap_display.WriteText(txt)
392 self.__soap_display.ShowPosition(0)
393
395
396
397 self.__root_context_popup = wx.Menu(title = _('EMR Actions:'))
398
399 menu_id = wx.NewId()
400 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Create health issue')))
401 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__create_issue)
402
403 item = self.__root_context_popup.Append(-1, _('Create episode'))
404 self.Bind(wx.EVT_MENU, self.__create_episode, item)
405
406 item = self.__root_context_popup.Append(-1, _('Create progress note'))
407 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item)
408
409 menu_id = wx.NewId()
410 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage allergies')))
411 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__document_allergy)
412
413 menu_id = wx.NewId()
414 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage family history')))
415 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_family_history)
416
417 menu_id = wx.NewId()
418 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage hospitalizations')))
419 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_hospital_stays)
420
421 menu_id = wx.NewId()
422 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage occupation')))
423 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_occupation)
424
425 menu_id = wx.NewId()
426 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage procedures')))
427 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_procedures)
428
429 menu_id = wx.NewId()
430 self.__root_context_popup.AppendItem(wx.MenuItem(self.__root_context_popup, menu_id, _('Manage vaccinations')))
431 wx.EVT_MENU(self.__root_context_popup, menu_id, self.__manage_vaccinations)
432
433 self.__root_context_popup.AppendSeparator()
434
435
436 expand_menu = wx.Menu()
437 self.__root_context_popup.AppendMenu(wx.NewId(), _('Open EMR to ...'), expand_menu)
438
439 menu_id = wx.NewId()
440 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... issue level')))
441 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_issue_level)
442
443 menu_id = wx.NewId()
444 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... episode level')))
445 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_episode_level)
446
447 menu_id = wx.NewId()
448 expand_menu.AppendItem(wx.MenuItem(expand_menu, menu_id, _('... encounter level')))
449 wx.EVT_MENU(expand_menu, menu_id, self.__expand_to_encounter_level)
450
451
452 self.__issue_context_popup = wx.Menu(title = _('Health Issue Actions:'))
453
454 menu_id = wx.NewId()
455 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Edit details')))
456 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__edit_issue)
457
458 menu_id = wx.NewId()
459 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Delete')))
460 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__delete_issue)
461
462 self.__issue_context_popup.AppendSeparator()
463
464 menu_id = wx.NewId()
465 self.__issue_context_popup.AppendItem(wx.MenuItem(self.__issue_context_popup, menu_id, _('Open to encounter level')))
466 wx.EVT_MENU(self.__issue_context_popup, menu_id, self.__expand_issue_to_encounter_level)
467
468
469
470 item = self.__issue_context_popup.Append(-1, _('Create progress note'))
471 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item)
472
473
474 self.__epi_context_popup = wx.Menu(title = _('Episode Actions:'))
475
476 menu_id = wx.NewId()
477 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Edit details')))
478 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__edit_episode)
479
480 menu_id = wx.NewId()
481 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Delete')))
482 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__delete_episode)
483
484 menu_id = wx.NewId()
485 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Promote')))
486 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__promote_episode_to_issue)
487
488 item = self.__epi_context_popup.Append(-1, _('Create progress note'))
489 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item)
490
491 menu_id = wx.NewId()
492 self.__epi_context_popup.AppendItem(wx.MenuItem(self.__epi_context_popup, menu_id, _('Move encounters')))
493 wx.EVT_MENU(self.__epi_context_popup, menu_id, self.__move_encounters)
494
495
496 self.__enc_context_popup = wx.Menu(title = _('Encounter Actions:'))
497
498 menu_id = wx.NewId()
499 self.__enc_context_popup.AppendItem(wx.MenuItem(self.__enc_context_popup, menu_id, _('Move data to another episode')))
500 wx.EVT_MENU(self.__enc_context_popup, menu_id, self.__relink_encounter_data2episode)
501
502 menu_id = wx.NewId()
503 self.__enc_context_popup.AppendItem(wx.MenuItem(self.__enc_context_popup, menu_id, _('Edit details')))
504 wx.EVT_MENU(self.__enc_context_popup, menu_id, self.__edit_encounter_details)
505
506
507
508
509
510 item = self.__enc_context_popup.Append(-1, _('Edit progress notes'))
511 self.Bind(wx.EVT_MENU, self.__edit_progress_notes, item)
512
513 item = self.__enc_context_popup.Append(-1, _('Move progress notes'))
514 self.Bind(wx.EVT_MENU, self.__move_progress_notes, item)
515
516 item = self.__enc_context_popup.Append(-1, _('Export for Medistar'))
517 self.Bind(wx.EVT_MENU, self.__export_encounter_for_medistar, item)
518
519 - def __handle_root_context(self, pos=wx.DefaultPosition):
520 self.PopupMenu(self.__root_context_popup, pos)
521
522 - def __handle_issue_context(self, pos=wx.DefaultPosition):
523
524 self.PopupMenu(self.__issue_context_popup, pos)
525
526 - def __handle_episode_context(self, pos=wx.DefaultPosition):
527
528 self.PopupMenu(self.__epi_context_popup, pos)
529
530 - def __handle_encounter_context(self, pos=wx.DefaultPosition):
531 self.PopupMenu(self.__enc_context_popup, pos)
532
533
534
543
546
550
552 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
553 parent = self,
554 id = -1,
555 caption = _('Deleting episode'),
556 button_defs = [
557 {'label': _('Yes, delete'), 'tooltip': _('Delete the episode if possible (it must be completely empty).')},
558 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the episode.')}
559 ],
560 question = _(
561 'Are you sure you want to delete this episode ?\n'
562 '\n'
563 ' "%s"\n'
564 ) % self.__curr_node_data['description']
565 )
566 result = dlg.ShowModal()
567 if result != wx.ID_YES:
568 return
569
570 if not gmEMRStructItems.delete_episode(episode = self.__curr_node_data):
571 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete episode. There is still clinical data recorded for it.'))
572
574 self.DeleteChildren(episode_node)
575
576 emr = self.__pat.emr
577 epi = self.GetPyData(episode_node)
578 encounters = emr.get_encounters(episodes = [epi['pk_episode']], skip_empty = True)
579 if len(encounters) == 0:
580 self.SetItemHasChildren(episode_node, False)
581 return
582
583 self.SetItemHasChildren(episode_node, True)
584
585 for enc in encounters:
586 label = u'%s: %s' % (
587 enc['started'].strftime('%Y-%m-%d'),
588 gmTools.unwrap (
589 gmTools.coalesce (
590 gmTools.coalesce (
591 gmTools.coalesce (
592 enc.get_latest_soap (
593 soap_cat = 'a',
594 episode = epi['pk_episode']
595 ),
596 enc['assessment_of_encounter']
597 ),
598 enc['reason_for_encounter']
599 ),
600 enc['l10n_type']
601 ),
602 max_length = 40
603 )
604 )
605 encounter_node = self.AppendItem(episode_node, label)
606 self.SetItemPyData(encounter_node, enc)
607
608 self.SetItemHasChildren(encounter_node, False)
609
610 self.SortChildren(episode_node)
611
612
613
624
626 encounter = self.GetPyData(self.__curr_node)
627 node_parent = self.GetItemParent(self.__curr_node)
628 episode = self.GetPyData(node_parent)
629
630 gmNarrativeWidgets.manage_progress_notes (
631 parent = self,
632 encounters = [encounter['pk_encounter']],
633 episodes = [episode['pk_episode']]
634 )
635
640
642
643 node_parent = self.GetItemParent(self.__curr_node)
644 owning_episode = self.GetPyData(node_parent)
645
646 episode_selector = gmNarrativeWidgets.cMoveNarrativeDlg (
647 self,
648 -1,
649 episode = owning_episode,
650 encounter = self.__curr_node_data
651 )
652
653 result = episode_selector.ShowModal()
654 episode_selector.Destroy()
655
656 if result == wx.ID_YES:
657 self.__populate_tree()
658
659
660
663
665 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
666 parent = self,
667 id = -1,
668 caption = _('Deleting health issue'),
669 button_defs = [
670 {'label': _('Yes, delete'), 'tooltip': _('Delete the health issue if possible (it must be completely empty).')},
671 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the health issue.')}
672 ],
673 question = _(
674 'Are you sure you want to delete this health issue ?\n'
675 '\n'
676 ' "%s"\n'
677 ) % self.__curr_node_data['description']
678 )
679 result = dlg.ShowModal()
680 if result != wx.ID_YES:
681 dlg.Destroy()
682 return
683
684 dlg.Destroy()
685
686 try:
687 gmEMRStructItems.delete_health_issue(health_issue = self.__curr_node_data)
688 except gmExceptions.DatabaseObjectInUseError:
689 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete health issue. There is still clinical data recorded for it.'))
690
692
693 if not self.__curr_node.IsOk():
694 return
695
696 self.Expand(self.__curr_node)
697
698 epi, epi_cookie = self.GetFirstChild(self.__curr_node)
699 while epi.IsOk():
700 self.Expand(epi)
701 epi, epi_cookie = self.GetNextChild(self.__curr_node, epi_cookie)
702
704 self.DeleteChildren(issue_node)
705
706 emr = self.__pat.emr
707 issue = self.GetPyData(issue_node)
708 episodes = emr.get_episodes(issues = [issue['pk_health_issue']])
709 if len(episodes) == 0:
710 self.SetItemHasChildren(issue_node, False)
711 return
712
713 self.SetItemHasChildren(issue_node, True)
714
715 for episode in episodes:
716 episode_node = self.AppendItem(issue_node, episode['description'])
717 self.SetItemPyData(episode_node, episode)
718
719 self.SetItemHasChildren(episode_node, True)
720
721 self.SortChildren(issue_node)
722
724 self.DeleteChildren(fake_issue_node)
725
726 emr = self.__pat.emr
727 episodes = emr.unlinked_episodes
728 if len(episodes) == 0:
729 self.SetItemHasChildren(fake_issue_node, False)
730 return
731
732 self.SetItemHasChildren(fake_issue_node, True)
733
734 for episode in episodes:
735 episode_node = self.AppendItem(fake_issue_node, episode['description'])
736 self.SetItemPyData(episode_node, episode)
737 if episode['episode_open']:
738 self.SetItemBold(fake_issue_node, True)
739
740 self.SetItemHasChildren(episode_node, True)
741
742 self.SortChildren(fake_issue_node)
743
744
745
748
751
753 self.__cb__select_edit_mode(True)
754 self.__cb__add_soap_editor(problem = self.__curr_node_data, allow_same_problem = False)
755
763
766
769
772
775
778
780
781 root_item = self.GetRootItem()
782
783 if not root_item.IsOk():
784 return
785
786 self.Expand(root_item)
787
788
789 issue, issue_cookie = self.GetFirstChild(root_item)
790 while issue.IsOk():
791 self.Collapse(issue)
792 epi, epi_cookie = self.GetFirstChild(issue)
793 while epi.IsOk():
794 self.Collapse(epi)
795 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
796 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
797
799
800 root_item = self.GetRootItem()
801
802 if not root_item.IsOk():
803 return
804
805 self.Expand(root_item)
806
807
808 issue, issue_cookie = self.GetFirstChild(root_item)
809 while issue.IsOk():
810 self.Expand(issue)
811 epi, epi_cookie = self.GetFirstChild(issue)
812 while epi.IsOk():
813 self.Collapse(epi)
814 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
815 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
816
818
819 root_item = self.GetRootItem()
820
821 if not root_item.IsOk():
822 return
823
824 self.Expand(root_item)
825
826
827 issue, issue_cookie = self.GetFirstChild(root_item)
828 while issue.IsOk():
829 self.Expand(issue)
830 epi, epi_cookie = self.GetFirstChild(issue)
831 while epi.IsOk():
832 self.Expand(epi)
833 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
834 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
835
842
844 root_node = self.GetRootItem()
845 self.DeleteChildren(root_node)
846
847 issues = [{
848 'description': _('Unattributed episodes'),
849 'has_open_episode': False,
850 'pk_health_issue': None
851 }]
852
853 emr = self.__pat.emr
854 issues.extend(emr.health_issues)
855
856 for issue in issues:
857 issue_node = self.AppendItem(root_node, issue['description'])
858 self.SetItemBold(issue_node, issue['has_open_episode'])
859 self.SetItemPyData(issue_node, issue)
860
861 self.SetItemHasChildren(issue_node, True)
862
863 self.SetItemHasChildren(root_node, (len(issues) != 0))
864 self.SortChildren(root_node)
865
866
867
869 wx.CallAfter(self.__update_text_for_selected_node)
870
872 wx.CallAfter(self.__populate_tree)
873
875 wx.CallAfter(self.__populate_tree)
876
878 if not self.__pat.connected:
879 return
880
881 event.Skip()
882
883 node = event.GetItem()
884 if node == self.GetRootItem():
885 self.__expand_root_node()
886 return
887
888 node_data = self.GetPyData(node)
889
890 if isinstance(node_data, gmEMRStructItems.cHealthIssue):
891 self.__expand_issue_node(issue_node = node)
892 return
893
894 if isinstance(node_data, gmEMRStructItems.cEpisode):
895 self.__expand_episode_node(episode_node = node)
896 return
897
898
899 if type(node_data) == type({}):
900 self.__expand_pseudo_issue_node(fake_issue_node = node)
901 return
902
903
904
905
907 sel_item = event.GetItem()
908 self.__curr_node = sel_item
909 self.__update_text_for_selected_node()
910 return True
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1062 """Right button clicked: display the popup for the tree"""
1063
1064 node = event.GetItem()
1065 self.SelectItem(node)
1066 self.__curr_node_data = self.GetPyData(node)
1067 self.__curr_node = node
1068
1069 pos = wx.DefaultPosition
1070 if isinstance(self.__curr_node_data, gmEMRStructItems.cHealthIssue):
1071 self.__handle_issue_context(pos=pos)
1072 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEpisode):
1073 self.__handle_episode_context(pos=pos)
1074 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEncounter):
1075 self.__handle_encounter_context(pos=pos)
1076 elif node == self.GetRootItem():
1077 self.__handle_root_context()
1078 elif type(self.__curr_node_data) == type({}):
1079
1080 pass
1081 else:
1082 print "error: unknown node type, no popup menu"
1083 event.Skip()
1084
1086 """Used in sorting items.
1087
1088 -1: 1 < 2
1089 0: 1 = 2
1090 1: 1 > 2
1091 """
1092
1093
1094 if not node1:
1095 _log.debug('invalid node 1')
1096 return 0
1097 if not node2:
1098 _log.debug('invalid node 2')
1099 return 0
1100
1101 if not node1.IsOk():
1102 _log.debug('invalid node 1')
1103 return 0
1104 if not node2.IsOk():
1105 _log.debug('invalid node 2')
1106 return 0
1107
1108 item1 = self.GetPyData(node1)
1109 item2 = self.GetPyData(node2)
1110
1111
1112 if isinstance(item1, type({})):
1113 return -1
1114 if isinstance(item2, type({})):
1115 return 1
1116
1117
1118 if isinstance(item1, gmEMRStructItems.cEncounter):
1119 if item1['started'] == item2['started']:
1120 return 0
1121 if item1['started'] > item2['started']:
1122 return -1
1123 return 1
1124
1125
1126 if isinstance(item1, gmEMRStructItems.cEpisode):
1127 start1 = item1.best_guess_start_date
1128 start2 = item2.best_guess_start_date
1129 if start1 == start2:
1130 return 0
1131 if start1 < start2:
1132 return -1
1133 return 1
1134
1135
1136 if isinstance(item1, gmEMRStructItems.cHealthIssue):
1137
1138
1139 if item1['grouping'] is None:
1140 if item2['grouping'] is not None:
1141 return 1
1142
1143
1144 if item1['grouping'] is not None:
1145 if item2['grouping'] is None:
1146 return -1
1147
1148
1149 if (item1['grouping'] is None) and (item2['grouping'] is None):
1150 if item1['description'].lower() < item2['description'].lower():
1151 return -1
1152 if item1['description'].lower() > item2['description'].lower():
1153 return 1
1154 return 0
1155
1156
1157 if item1['grouping'] < item2['grouping']:
1158 return -1
1159
1160 if item1['grouping'] > item2['grouping']:
1161 return 1
1162
1163 if item1['description'].lower() < item2['description'].lower():
1164 return -1
1165
1166 if item1['description'].lower() > item2['description'].lower():
1167 return 1
1168
1169 return 0
1170
1171 _log.error('unknown item type during sorting EMR tree:')
1172 _log.error('item1: %s', type(item1))
1173 _log.error('item2: %s', type(item2))
1174
1175 return 0
1176
1177
1178
1180 return self.__soap_display_mode
1181
1183 if mode not in [u'details', u'journal']:
1184 raise ValueError('details display mode must be one of "details", "journal"')
1185 if self.__soap_display_mode == mode:
1186 return
1187 self.__soap_display_mode = mode
1188 self.__update_text_for_selected_node()
1189
1190 details_display_mode = property(_get_details_display_mode, _set_details_display_mode)
1191
1192 from Gnumed.wxGladeWidgets import wxgScrolledEMRTreePnl
1193
1207
1208 from Gnumed.wxGladeWidgets import wxgSplittedEMRTreeBrowserPnl
1209
1211 """A splitter window holding an EMR tree.
1212
1213 The left hand side displays a scrollable EMR tree while
1214 on the right details for selected items are displayed.
1215
1216 Expects to be put into a Notebook.
1217 """
1228
1230 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1231 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1232 return True
1233
1235 return self.__editing
1236
1238 self.__editing = editing
1239 self.enable_display_mode_selection(enable = not self.__editing)
1240 if self.__editing:
1241 self._BTN_switch_browse_edit.SetLabel(_('&Browse %s') % gmTools.u_ellipsis)
1242 self._PNL_browse.Hide()
1243 self._PNL_visual_soap.Hide()
1244 self._PNL_edit.Show()
1245 else:
1246 self._BTN_switch_browse_edit.SetLabel(_('&New notes %s') % gmTools.u_ellipsis)
1247 self._PNL_edit.Hide()
1248 self._PNL_visual_soap.Show()
1249 self._PNL_browse.Show()
1250 self._PNL_right_side.GetSizer().Layout()
1251
1252 editing = property(_get_editing, _set_editing)
1253
1254
1255
1257 self._pnl_emr_tree._emr_tree.clear_tree()
1258 self._PNL_edit.patient = None
1259 return True
1260
1262 wx.CallAfter(self.__on_post_patient_selection)
1263 return True
1264
1266 if self.GetParent().GetCurrentPage() != self:
1267 return True
1268 self.repopulate_ui()
1269
1272
1275
1278
1279
1280
1282 """Fills UI with data."""
1283 self._pnl_emr_tree.repopulate_ui()
1284 self._PNL_edit.patient = gmPerson.gmCurrentPatient()
1285 self._splitter_browser.SetSashPosition(self._splitter_browser.GetSizeTuple()[0]/3, True)
1286 return True
1287
1289 if self.editing:
1290 enable = False
1291 if enable:
1292 self._RBTN_details.Enable(True)
1293 self._RBTN_journal.Enable(True)
1294 return
1295 self._RBTN_details.Enable(False)
1296 self._RBTN_journal.Enable(False)
1297
1299 self._PNL_edit._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_same_problem)
1300
1303
1304
1305 from Gnumed.wxGladeWidgets import wxgEMRJournalPluginPnl
1306
1308
1313
1314
1315
1317 self._TCTRL_journal.SetValue(u'')
1318 exporter = gmPatientExporter.cEMRJournalExporter()
1319 if self._RBTN_by_encounter.GetValue():
1320 txt = StringIO.StringIO()
1321
1322
1323 try:
1324 exporter.export(txt)
1325 self._TCTRL_journal.SetValue(txt.getvalue())
1326 except ValueError:
1327 _log.exception('cannot get EMR journal')
1328 self._TCTRL_journal.SetValue (_(
1329 'An error occurred while retrieving the EMR\n'
1330 'in journal form for the active patient.\n\n'
1331 'Please check the log file for details.'
1332 ))
1333 txt.close()
1334 else:
1335 fname = exporter.export_to_file_by_mod_time()
1336 f = codecs.open(filename = fname, mode = 'rU', encoding = 'utf8', errors = 'replace')
1337 for line in f:
1338 self._TCTRL_journal.AppendText(line)
1339 f.close()
1340
1341 self._TCTRL_journal.ShowPosition(self._TCTRL_journal.GetLastPosition())
1342 return True
1343
1344
1345
1347 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1348 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1349 return True
1350
1351
1352
1354 self._TCTRL_journal.SetValue(u'')
1355 return True
1356
1358 wx.CallAfter(self.__on_post_patient_selection)
1359 return True
1360
1362 if self.GetParent().GetCurrentPage() != self:
1363 return True
1364 self.repopulate_ui()
1365
1368
1371
1372
1373
1374
1375 if __name__ == '__main__':
1376
1377 _log.info("starting emr browser...")
1378
1379 try:
1380
1381 patient = gmPersonSearch.ask_for_patient()
1382 if patient is None:
1383 print "No patient. Exiting gracefully..."
1384 sys.exit(0)
1385 gmPatSearchWidgets.set_active_patient(patient = patient)
1386
1387
1388 application = wx.PyWidgetTester(size=(800,600))
1389 emr_browser = cEMRBrowserPanel(application.frame, -1)
1390 emr_browser.refresh_tree()
1391
1392 application.frame.Show(True)
1393 application.MainLoop()
1394
1395
1396 if patient is not None:
1397 try:
1398 patient.cleanup()
1399 except:
1400 print "error cleaning up patient"
1401 except StandardError:
1402 _log.exception("unhandled exception caught !")
1403
1404 raise
1405
1406 _log.info("closing emr browser...")
1407
1408
1409