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 io
10 import logging
11
12
13
14 import wx
15 import wx.lib.mixins.treemixin as treemixin
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 from Gnumed.business import gmSoapDefs
33 from Gnumed.business import gmClinicalRecord
34
35 from Gnumed.wxpython import gmGuiHelpers
36 from Gnumed.wxpython import gmEMRStructWidgets
37 from Gnumed.wxpython import gmEncounterWidgets
38 from Gnumed.wxpython import gmSOAPWidgets
39 from Gnumed.wxpython import gmAllergyWidgets
40 from Gnumed.wxpython import gmDemographicsWidgets
41 from Gnumed.wxpython import gmNarrativeWidgets
42 from Gnumed.wxpython import gmNarrativeWorkflows
43 from Gnumed.wxpython import gmPatSearchWidgets
44 from Gnumed.wxpython import gmVaccWidgets
45 from Gnumed.wxpython import gmFamilyHistoryWidgets
46 from Gnumed.wxpython import gmFormWidgets
47 from Gnumed.wxpython import gmTimer
48 from Gnumed.wxpython import gmHospitalStayWidgets
49 from Gnumed.wxpython import gmProcedureWidgets
50
51
52 _log = logging.getLogger('gm.ui')
53
54
56 """
57 Dump the patient's EMR from GUI client
58 @param parent - The parent widget
59 @type parent - A wx.Window instance
60 """
61
62 if parent is None:
63 raise TypeError('expected wx.Window instance as parent, got <None>')
64
65 pat = gmPerson.gmCurrentPatient()
66 if not pat.connected:
67 gmDispatcher.send(signal='statustext', msg=_('Cannot export EMR. No active patient.'))
68 return False
69
70
71 wc = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files"))
72 defdir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed', pat.subdir_name)))
73 gmTools.mkdir(defdir)
74 fname = '%s-%s_%s.txt' % (_('emr-export'), pat['lastnames'], pat['firstnames'])
75 dlg = wx.FileDialog (
76 parent = parent,
77 message = _("Save patient's EMR as..."),
78 defaultDir = defdir,
79 defaultFile = fname,
80 wildcard = wc,
81 style = wx.FD_SAVE
82 )
83 choice = dlg.ShowModal()
84 fname = dlg.GetPath()
85 dlg.Destroy()
86 if choice != wx.ID_OK:
87 return None
88
89 _log.debug('exporting EMR to [%s]', fname)
90
91 output_file = io.open(fname, mode = 'wt', encoding = 'utf8', errors = 'replace')
92 exporter = gmPatientExporter.cEmrExport(patient = pat)
93 exporter.set_output_file(output_file)
94 exporter.dump_constraints()
95 exporter.dump_demographic_record(True)
96 exporter.dump_clinical_record()
97 exporter.dump_med_docs()
98 output_file.close()
99
100 gmDispatcher.send('statustext', msg = _('EMR successfully exported to file: %s') % fname, beep = False)
101 return fname
102
103
104 -class cEMRTree(wx.TreeCtrl, treemixin.ExpansionState):
105 """This wx.TreeCtrl derivative displays a tree view of a medical record."""
106
107
108 - def __init__(self, parent, id, *args, **kwds):
109 """Set up our specialised tree.
110 """
111 kwds['style'] = wx.TR_HAS_BUTTONS | wx.NO_BORDER | wx.TR_SINGLE
112 wx.TreeCtrl.__init__(self, parent, id, *args, **kwds)
113
114 self.__soap_display = None
115 self.__soap_display_mode = 'details'
116 self.__img_display = None
117 self.__cb__enable_display_mode_selection = lambda x:x
118 self.__cb__select_edit_mode = lambda x:x
119 self.__cb__add_soap_editor = lambda x:x
120 self.__pat = None
121 self.__curr_node = None
122 self.__expanded_nodes = None
123
124 self.__make_popup_menus()
125 self.__register_events()
126
127
128
129
131 return self.__soap_display
132
134 self.__soap_display = soap_display
135 self.__soap_display_prop_font = soap_display.GetFont()
136 self.__soap_display_mono_font = wx.Font(self.__soap_display_prop_font.GetNativeFontInfo())
137 self.__soap_display_mono_font.SetFamily(wx.FONTFAMILY_TELETYPE)
138 self.__soap_display_mono_font.SetPointSize(self.__soap_display_prop_font.GetPointSize() - 2)
139
140 soap_display = property(_get_soap_display, _set_soap_display)
141
142
144 return self.__img_display
145
148
149 image_display = property(_get_image_display, _set_image_display)
150
151
153 if not callable(callback):
154 raise ValueError('callback [%s] not callable' % callback)
155 self.__cb__enable_display_mode_selection = callback
156
157
159 if callback is None:
160 callback = lambda x:x
161 if not callable(callback):
162 raise ValueError('edit mode selector [%s] not callable' % callback)
163 self.__cb__select_edit_mode = callback
164
165 edit_mode_selector = property(lambda x:x, _set_edit_mode_selector)
166
167
169 if callback is None:
170 callback = lambda x:x
171 if not callable(callback):
172 raise ValueError('soap editor adder [%s] not callable' % callback)
173 self.__cb__add_soap_editor = callback
174
175 soap_editor_adder = property(lambda x:x, _set_soap_editor_adder)
176
177
178
179
181 if item is None:
182 return 'invalid item'
183
184 if not item.IsOk():
185 return 'invalid item'
186
187 try:
188 node_data = self.GetItemData(item)
189 except wx.wxAssertionError:
190 _log.exception('unfathomable self.GetItemData() problem occurred, faking root node')
191 _log.error('real node: %s', item)
192 _log.error('node.IsOk(): %s', item.IsOk())
193 _log.error('is root node: %s', item == self.GetRootItem())
194 _log.error('node attributes: %s', dir(item))
195 gmLog2.log_stack_trace()
196 return 'invalid item'
197
198 if isinstance(node_data, gmEMRStructItems.cHealthIssue):
199 return 'issue::%s' % node_data['pk_health_issue']
200 if isinstance(node_data, gmEMRStructItems.cEpisode):
201 return 'episode::%s' % node_data['pk_episode']
202 if isinstance(node_data, gmEMRStructItems.cEncounter):
203 return 'encounter::%s' % node_data['pk_encounter']
204
205 if isinstance(node_data, type({})):
206 return 'dummy node::%s' % self.__pat.ID
207
208 return 'root node::%s' % self.__pat.ID
209
210
211
212
214 """Configures enabled event signals."""
215 self.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_tree_item_selected)
216 self.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self._on_tree_item_right_clicked)
217 self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self._on_tree_item_expanding)
218
219
220
221 self.Bind(wx.EVT_TREE_ITEM_GETTOOLTIP, self._on_tree_item_gettooltip)
222
223
224 gmDispatcher.connect(signal = 'narrative_mod_db', receiver = self._on_narrative_mod_db)
225 gmDispatcher.connect(signal = 'clin.episode_mod_db', receiver = self._on_episode_mod_db)
226 gmDispatcher.connect(signal = 'clin.health_issue_mod_db', receiver = self._on_issue_mod_db)
227 gmDispatcher.connect(signal = 'clin.family_history_mod_db', receiver = self._on_issue_mod_db)
228
229
231 self.DeleteAllItems()
232 self.__expanded_nodes = None
233
234
236 """Updates EMR browser data."""
237
238
239
240 _log.debug('populating EMR tree')
241
242 wx.BeginBusyCursor()
243
244 if self.__pat is None:
245 self.clear_tree()
246 self.__expanded_nodes = None
247 wx.EndBusyCursor()
248 return True
249
250
251 root_item = self.__populate_root_node()
252 self.__curr_node = root_item
253 if self.__expanded_nodes is not None:
254 self.ExpansionState = self.__expanded_nodes
255 self.SelectItem(root_item)
256 self.Expand(root_item)
257 self.__update_text_for_selected_node()
258
259 wx.EndBusyCursor()
260 return True
261
262
264
265 self.DeleteAllItems()
266
267 root_item = self.AddRoot(_('EMR of %(lastnames)s, %(firstnames)s') % self.__pat.get_active_name())
268 self.SetItemData(root_item, None)
269 self.SetItemHasChildren(root_item, True)
270
271 self.__root_tooltip = self.__pat['description_gender'] + '\n'
272 if self.__pat['deceased'] is None:
273 self.__root_tooltip += ' %s (%s)\n\n' % (
274 self.__pat.get_formatted_dob(format = '%d %b %Y'),
275 self.__pat['medical_age']
276 )
277 else:
278 template = ' %s - %s (%s)\n\n'
279 self.__root_tooltip += template % (
280 self.__pat.get_formatted_dob(format = '%d.%b %Y'),
281 gmDateTime.pydt_strftime(self.__pat['deceased'], '%Y %b %d'),
282 self.__pat['medical_age']
283 )
284 self.__root_tooltip += gmTools.coalesce(self.__pat['comment'], '', '%s\n\n')
285 doc = self.__pat.primary_provider
286 if doc is not None:
287 self.__root_tooltip += '%s:\n' % _('Primary provider in this praxis')
288 self.__root_tooltip += ' %s %s %s (%s)%s\n\n' % (
289 gmTools.coalesce(doc['title'], gmPerson.map_gender2salutation(gender = doc['gender'])),
290 doc['firstnames'],
291 doc['lastnames'],
292 doc['short_alias'],
293 gmTools.bool2subst(doc['is_active'], '', ' [%s]' % _('inactive'))
294 )
295 if not ((self.__pat['emergency_contact'] is None) and (self.__pat['pk_emergency_contact'] is None)):
296 self.__root_tooltip += _('In case of emergency contact:') + '\n'
297 if self.__pat['emergency_contact'] is not None:
298 self.__root_tooltip += gmTools.wrap (
299 text = '%s\n' % self.__pat['emergency_contact'],
300 width = 60,
301 initial_indent = ' ',
302 subsequent_indent = ' '
303 )
304 if self.__pat['pk_emergency_contact'] is not None:
305 contact = self.__pat.emergency_contact_in_database
306 self.__root_tooltip += ' %s\n' % contact['description_gender']
307 self.__root_tooltip = self.__root_tooltip.strip('\n')
308 if self.__root_tooltip == '':
309 self.__root_tooltip = ' '
310
311 return root_item
312
313
315 """Displays information for the selected tree node."""
316
317 if self.__soap_display is None:
318 return
319
320 self.__soap_display.Clear()
321 self.__img_display.clear()
322
323 if self.__curr_node is None:
324 return
325
326 if not self.__curr_node.IsOk():
327 return
328
329 try:
330 node_data = self.GetItemData(self.__curr_node)
331 except wx.wxAssertionError:
332 node_data = None
333 _log.exception('unfathomable self.GetItemData() problem occurred, faking root node')
334 _log.error('real node: %s', self.__curr_node)
335 _log.error('node.IsOk(): %s', self.__curr_node.IsOk())
336 _log.error('is root node: %s', self.__curr_node == self.GetRootItem())
337 _log.error('node attributes: %s', dir(self.__curr_node))
338 gmLog2.log_stack_trace()
339
340 doc_folder = self.__pat.get_document_folder()
341
342 if isinstance(node_data, gmEMRStructItems.cHealthIssue):
343 self.__cb__enable_display_mode_selection(True)
344 txt = 'invalid SOAP display mode [%s]' % self.__soap_display_mode
345 if self.__soap_display_mode == 'details':
346 txt = node_data.format(left_margin = 1, patient = self.__pat)
347 font = self.__soap_display_prop_font
348 if self.__soap_display_mode == 'journal':
349 txt = node_data.format_as_journal(left_margin = 1)
350 font = self.__soap_display_prop_font
351 if self.__soap_display_mode == 'revisions':
352 txt = node_data.formatted_revision_history
353 font = self.__soap_display_mono_font
354 epis = node_data.episodes
355 if len(epis) > 0:
356 self.__img_display.refresh (
357 document_folder = doc_folder,
358 episodes = [ epi['pk_episode'] for epi in epis ],
359 async = True
360 )
361 self.__soap_display.SetFont(font)
362 self.__soap_display.WriteText(txt)
363 self.__soap_display.ShowPosition(0)
364 return
365
366
367 if isinstance(node_data, type({})):
368 self.__cb__enable_display_mode_selection(True)
369 if self.__soap_display_mode == 'details':
370 txt = _('Pool of unassociated episodes "%s":\n') % node_data['description']
371 epis = self.__pat.emr.get_episodes(unlinked_only = True, order_by = 'episode_open DESC, description')
372 if len(epis) > 0:
373 txt += '\n'
374 for epi in epis:
375 txt += epi.format (
376 left_margin = 1,
377 patient = self.__pat,
378 with_summary = True,
379 with_codes = False,
380 with_encounters = False,
381 with_documents = False,
382 with_hospital_stays = False,
383 with_procedures = False,
384 with_family_history = False,
385 with_tests = False,
386 with_vaccinations = False,
387 with_health_issue = False
388 )
389 txt += '\n'
390 else:
391 epis = self.__pat.emr.get_episodes(unlinked_only = True, order_by = 'episode_open DESC, description')
392 txt = ''
393 if len(epis) > 0:
394 txt += _(' Listing of unassociated episodes\n')
395 for epi in epis:
396 txt += ' %s\n' % (gmTools.u_box_horiz_4dashes * 60)
397 txt += epi.format (
398 left_margin = 1,
399 patient = self.__pat,
400 with_summary = False,
401 with_codes = False,
402 with_encounters = False,
403 with_documents = False,
404 with_hospital_stays = False,
405 with_procedures = False,
406 with_family_history = False,
407 with_tests = False,
408 with_vaccinations = False,
409 with_health_issue = False
410 )
411 txt += '\n'
412 txt += epi.format_as_journal(left_margin = 2)
413 self.__soap_display.SetFont(self.__soap_display_prop_font)
414 self.__soap_display.WriteText(txt)
415 self.__soap_display.ShowPosition(0)
416 return
417
418 if isinstance(node_data, gmEMRStructItems.cEpisode):
419 self.__cb__enable_display_mode_selection(True)
420 txt = 'invalid SOAP display mode [%s]' % self.__soap_display_mode
421 if self.__soap_display_mode == 'details':
422 txt = node_data.format(left_margin = 1, patient = self.__pat)
423 font = self.__soap_display_prop_font
424 if self.__soap_display_mode == 'journal':
425 txt = node_data.format_as_journal(left_margin = 1)
426 font = self.__soap_display_prop_font
427 if self.__soap_display_mode == 'revisions':
428 txt = node_data.formatted_revision_history
429 font = self.__soap_display_mono_font
430 self.__img_display.refresh (
431 document_folder = doc_folder,
432 episodes = [ node_data['pk_episode'] ]
433 )
434 self.__soap_display.SetFont(font)
435 self.__soap_display.WriteText(txt)
436 self.__soap_display.ShowPosition(0)
437 return
438
439 if isinstance(node_data, gmEMRStructItems.cEncounter):
440 self.__cb__enable_display_mode_selection(True)
441 epi = self.GetItemData(self.GetItemParent(self.__curr_node))
442 if self.__soap_display_mode == 'revisions':
443 txt = node_data.formatted_revision_history
444 font = self.__soap_display_mono_font
445 else:
446 txt = node_data.format (
447 episodes = [epi['pk_episode']],
448 with_soap = True,
449 left_margin = 1,
450 patient = self.__pat,
451 with_co_encountlet_hints = True
452 )
453 font = self.__soap_display_prop_font
454 self.__img_display.refresh (
455 document_folder = doc_folder,
456 episodes = [ epi['pk_episode'] ],
457 encounter = node_data['pk_encounter']
458 )
459 self.__soap_display.SetFont(font)
460 self.__soap_display.WriteText(txt)
461 self.__soap_display.ShowPosition(0)
462 return
463
464
465 self.__cb__enable_display_mode_selection(True)
466 if self.__soap_display_mode == 'details':
467 emr = self.__pat.emr
468 txt = emr.format_summary()
469 else:
470 txt = self.__pat.emr.format_as_journal(left_margin = 1, patient = self.__pat)
471 self.__soap_display.SetFont(self.__soap_display_prop_font)
472 self.__soap_display.WriteText(txt)
473 self.__soap_display.ShowPosition(0)
474
475
477
478
479 self.__root_context_popup = wx.Menu(title = _('EMR Actions:'))
480 item = self.__root_context_popup.Append(-1, _('Print EMR'))
481 self.Bind(wx.EVT_MENU, self.__print_emr, item)
482 item = self.__root_context_popup.Append(-1, _('Create health issue'))
483 self.Bind(wx.EVT_MENU, self.__create_issue, item)
484 item = self.__root_context_popup.Append(-1, _('Create episode'))
485 self.Bind(wx.EVT_MENU, self.__create_episode, item)
486 item = self.__root_context_popup.Append(-1, _('Create progress note'))
487 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item)
488 item = self.__root_context_popup.Append(-1, _('Manage allergies'))
489 self.Bind(wx.EVT_MENU, self.__document_allergy, item)
490 item = self.__root_context_popup.Append(-1, _('Manage family history'))
491 self.Bind(wx.EVT_MENU, self.__manage_family_history, item)
492 item = self.__root_context_popup.Append(-1, _('Manage hospitalizations'))
493 self.Bind(wx.EVT_MENU, self.__manage_hospital_stays, item)
494 item = self.__root_context_popup.Append(-1, _('Manage occupation'))
495 self.Bind(wx.EVT_MENU, self.__manage_occupation, item)
496 item = self.__root_context_popup.Append(-1, _('Manage procedures'))
497 self.Bind(wx.EVT_MENU, self.__manage_procedures, item)
498 item = self.__root_context_popup.Append(-1, _('Manage vaccinations'))
499 self.Bind(wx.EVT_MENU, self.__manage_vaccinations, item)
500
501 self.__root_context_popup.AppendSeparator()
502
503
504 expand_menu = wx.Menu()
505 self.__root_context_popup.Append(wx.NewId(), _('Open EMR to ...'), expand_menu)
506 item = expand_menu.Append(-1, _('... issue level'))
507 self.Bind(wx.EVT_MENU, self.__expand_to_issue_level, item)
508 item = expand_menu.Append(-1, _('... episode level'))
509 self.Bind(wx.EVT_MENU, self.__expand_to_episode_level, item)
510 item = expand_menu.Append(-1, _('... encounter level'))
511 self.Bind(wx.EVT_MENU, self.__expand_to_encounter_level, item)
512
513
514 self.__issue_context_popup = wx.Menu(title = _('Health Issue Actions:'))
515 item = self.__issue_context_popup.Append(-1, _('Edit details'))
516 self.Bind(wx.EVT_MENU, self.__edit_issue, item)
517 item = self.__issue_context_popup.Append(-1, _('Delete'))
518 self.Bind(wx.EVT_MENU, self.__delete_issue, item)
519 self.__issue_context_popup.AppendSeparator()
520 item = self.__issue_context_popup.Append(-1, _('Open to encounter level'))
521 self.Bind(wx.EVT_MENU, self.__expand_issue_to_encounter_level, item)
522
523
524 item = self.__issue_context_popup.Append(-1, _('Create progress note'))
525 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item)
526
527
528 self.__epi_context_popup = wx.Menu(title = _('Episode Actions:'))
529 item = self.__epi_context_popup.Append(-1, _('Toggle ongoing/closed'))
530 self.Bind(wx.EVT_MENU, self.__toggle_episode_open_close, item)
531 item = self.__epi_context_popup.Append(-1, _('Edit details'))
532 self.Bind(wx.EVT_MENU, self.__edit_episode, item)
533 item = self.__epi_context_popup.Append(-1, _('Delete'))
534 self.Bind(wx.EVT_MENU, self.__delete_episode, item)
535 item = self.__epi_context_popup.Append(-1, _('Promote'))
536 self.Bind(wx.EVT_MENU, self.__promote_episode_to_issue, item)
537 item = self.__epi_context_popup.Append(-1, _('Create progress note'))
538 self.Bind(wx.EVT_MENU, self.__create_soap_editor, item)
539 item = self.__epi_context_popup.Append(-1, _('Move encounters'))
540 self.Bind(wx.EVT_MENU, self.__move_encounters, item)
541
542
543 self.__enc_context_popup = wx.Menu(title = _('Encounter Actions:'))
544 item = self.__enc_context_popup.Append(-1, _('Move data to another episode'))
545 self.Bind(wx.EVT_MENU, self.__relink_encounter_data2episode, item)
546 item = self.__enc_context_popup.Append(-1, _('Edit details'))
547 self.Bind(wx.EVT_MENU, self.__edit_encounter_details, item)
548
549
550
551 item = self.__enc_context_popup.Append(-1, _('Edit progress notes'))
552 self.Bind(wx.EVT_MENU, self.__edit_progress_notes, item)
553 item = self.__enc_context_popup.Append(-1, _('Move progress notes'))
554 self.Bind(wx.EVT_MENU, self.__move_progress_notes, item)
555 item = self.__enc_context_popup.Append(-1, _('Export for Medistar'))
556 self.Bind(wx.EVT_MENU, self.__export_encounter_for_medistar, item)
557
558
559 - def __handle_root_context(self, pos=wx.DefaultPosition):
560 self.PopupMenu(self.__root_context_popup, pos)
561
562
563 - def __handle_issue_context(self, pos=wx.DefaultPosition):
564 self.PopupMenu(self.__issue_context_popup, pos)
565
566
567 - def __handle_episode_context(self, pos=wx.DefaultPosition):
568 self.PopupMenu(self.__epi_context_popup, pos)
569
570
571 - def __handle_encounter_context(self, pos=wx.DefaultPosition):
572 self.PopupMenu(self.__enc_context_popup, pos)
573
574
575
576
585
586
588 self.__curr_node_data['episode_open'] = not self.__curr_node_data['episode_open']
589 self.__curr_node_data.save()
590
591
594
595
598
599
601 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
602 parent = self,
603 id = -1,
604 caption = _('Deleting episode'),
605 button_defs = [
606 {'label': _('Yes, delete'), 'tooltip': _('Delete the episode if possible (it must be completely empty).')},
607 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the episode.')}
608 ],
609 question = _(
610 'Are you sure you want to delete this episode ?\n'
611 '\n'
612 ' "%s"\n'
613 ) % self.__curr_node_data['description']
614 )
615 result = dlg.ShowModal()
616 if result != wx.ID_YES:
617 return
618
619 if not gmEMRStructItems.delete_episode(episode = self.__curr_node_data):
620 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete episode. There is still clinical data recorded for it.'))
621
622
624 self.DeleteChildren(episode_node)
625
626 emr = self.__pat.emr
627 epi = self.GetItemData(episode_node)
628 encounters = emr.get_encounters(episodes = [epi['pk_episode']], skip_empty = True)
629 if len(encounters) == 0:
630 self.SetItemHasChildren(episode_node, False)
631 return
632
633 self.SetItemHasChildren(episode_node, True)
634
635 for enc in encounters:
636 label = '%s: %s' % (
637 enc['started'].strftime('%Y-%m-%d'),
638 gmTools.unwrap (
639 gmTools.coalesce (
640 gmTools.coalesce (
641 gmTools.coalesce (
642 enc.get_latest_soap (
643 soap_cat = 'a',
644 episode = epi['pk_episode']
645 ),
646 enc['assessment_of_encounter']
647 ),
648 enc['reason_for_encounter']
649 ),
650 enc['l10n_type']
651 ),
652 max_length = 40
653 )
654 )
655 encounter_node = self.AppendItem(episode_node, label)
656 self.SetItemData(encounter_node, enc)
657
658 self.SetItemHasChildren(encounter_node, False)
659
660 self.SortChildren(episode_node)
661
662
663
664
675
676
678 encounter = self.GetItemData(self.__curr_node)
679 node_parent = self.GetItemParent(self.__curr_node)
680 episode = self.GetItemData(node_parent)
681
682 gmNarrativeWorkflows.manage_progress_notes (
683 parent = self,
684 encounters = [encounter['pk_encounter']],
685 episodes = [episode['pk_episode']]
686 )
687
688
693
694
696
697 node_parent = self.GetItemParent(self.__curr_node)
698 owning_episode = self.GetItemData(node_parent)
699
700 episode_selector = gmNarrativeWidgets.cMoveNarrativeDlg (
701 self,
702 -1,
703 episode = owning_episode,
704 encounter = self.__curr_node_data
705 )
706
707 result = episode_selector.ShowModal()
708 episode_selector.Destroy()
709
710 if result == wx.ID_YES:
711 self.__populate_tree()
712
713
714
715
718
719
721 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
722 parent = self,
723 id = -1,
724 caption = _('Deleting health issue'),
725 button_defs = [
726 {'label': _('Yes, delete'), 'tooltip': _('Delete the health issue if possible (it must be completely empty).')},
727 {'label': _('No, cancel'), 'tooltip': _('Cancel and do NOT delete the health issue.')}
728 ],
729 question = _(
730 'Are you sure you want to delete this health issue ?\n'
731 '\n'
732 ' "%s"\n'
733 ) % self.__curr_node_data['description']
734 )
735 result = dlg.ShowModal()
736 if result != wx.ID_YES:
737 dlg.Destroy()
738 return
739
740 dlg.Destroy()
741
742 if not gmEMRStructItems.delete_health_issue(health_issue = self.__curr_node_data):
743 gmDispatcher.send(signal = 'statustext', msg = _('Cannot delete health issue. There is still clinical data recorded for it.'))
744
745
747
748 if not self.__curr_node.IsOk():
749 return
750
751 self.Expand(self.__curr_node)
752
753 epi, epi_cookie = self.GetFirstChild(self.__curr_node)
754 while epi.IsOk():
755 self.Expand(epi)
756 epi, epi_cookie = self.GetNextChild(self.__curr_node, epi_cookie)
757
758
760 self.DeleteChildren(issue_node)
761
762 issue = self.GetItemData(issue_node)
763 episodes = self.__pat.emr.get_episodes(issues = [issue['pk_health_issue']])
764 if len(episodes) == 0:
765 self.SetItemHasChildren(issue_node, False)
766 return
767
768 self.SetItemHasChildren(issue_node, True)
769
770 for episode in episodes:
771 range_str, range_str_verb, duration_str = episode.formatted_clinical_duration
772 episode_node = self.AppendItem(issue_node, '%s (%s)' % (
773 episode['description'],
774 range_str
775 ))
776 self.SetItemData(episode_node, episode)
777
778 self.SetItemHasChildren(episode_node, True)
779
780 self.SortChildren(issue_node)
781
782
784 self.DeleteChildren(fake_issue_node)
785
786 episodes = self.__pat.emr.unlinked_episodes
787 if len(episodes) == 0:
788 self.SetItemHasChildren(fake_issue_node, False)
789 return
790
791 self.SetItemHasChildren(fake_issue_node, True)
792
793 for episode in episodes:
794 range_str, range_str_verb, duration_str = episode.formatted_clinical_duration
795 episode_node = self.AppendItem(fake_issue_node, '%s (%s)' % (
796 episode['description'],
797 range_str
798 ))
799 self.SetItemData(episode_node, episode)
800 if episode['episode_open']:
801 self.SetItemBold(fake_issue_node, True)
802
803 self.SetItemHasChildren(episode_node, True)
804
805 self.SortChildren(fake_issue_node)
806
807
808
809
812
813
816
817
820
821
823 self.__cb__select_edit_mode(True)
824 self.__cb__add_soap_editor(problem = self.__curr_node_data, allow_same_problem = False)
825
826
835
836
839
840
843
844
847
848
851
852
855
856
858
859 root_item = self.GetRootItem()
860
861 if not root_item.IsOk():
862 return
863
864 self.Expand(root_item)
865
866
867 issue, issue_cookie = self.GetFirstChild(root_item)
868 while issue.IsOk():
869 self.Collapse(issue)
870 epi, epi_cookie = self.GetFirstChild(issue)
871 while epi.IsOk():
872 self.Collapse(epi)
873 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
874 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
875
876
878
879 root_item = self.GetRootItem()
880
881 if not root_item.IsOk():
882 return
883
884 self.Expand(root_item)
885
886
887 issue, issue_cookie = self.GetFirstChild(root_item)
888 while issue.IsOk():
889 self.Expand(issue)
890 epi, epi_cookie = self.GetFirstChild(issue)
891 while epi.IsOk():
892 self.Collapse(epi)
893 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
894 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
895
896
898
899 root_item = self.GetRootItem()
900
901 if not root_item.IsOk():
902 return
903
904 self.Expand(root_item)
905
906
907 issue, issue_cookie = self.GetFirstChild(root_item)
908 while issue.IsOk():
909 self.Expand(issue)
910 epi, epi_cookie = self.GetFirstChild(issue)
911 while epi.IsOk():
912 self.Expand(epi)
913 epi, epi_cookie = self.GetNextChild(issue, epi_cookie)
914 issue, issue_cookie = self.GetNextChild(root_item, issue_cookie)
915
916
923
925 root_node = self.GetRootItem()
926 self.DeleteChildren(root_node)
927
928 issues = [{
929 'description': _('Unattributed episodes'),
930 'laterality': None,
931 'diagnostic_certainty_classification': None,
932 'has_open_episode': False,
933 'pk_health_issue': None
934 }]
935 issues.extend(self.__pat.emr.health_issues)
936 for issue in issues:
937 issue_node = self.AppendItem(root_node, '%s%s%s' % (
938 issue['description'],
939 gmTools.coalesce(issue['laterality'], '', ' [%s]', none_equivalents = [None, 'na']),
940 gmTools.coalesce(issue['diagnostic_certainty_classification'], '', ' [%s]')
941 ))
942 self.SetItemBold(issue_node, issue['has_open_episode'])
943 self.SetItemData(issue_node, issue)
944
945 self.SetItemHasChildren(issue_node, True)
946
947 self.SetItemHasChildren(root_node, (len(issues) != 0))
948 self.SortChildren(root_node)
949
950
951
952
954 self.__update_text_for_selected_node()
955
956
958 self.__expanded_nodes = self.ExpansionState
959 self.__populate_tree()
960
961
963 self.__expanded_nodes = self.ExpansionState
964 self.__populate_tree()
965
966
968 event.Skip()
969
970 node = event.GetItem()
971 if node == self.GetRootItem():
972 self.__expand_root_node()
973 return
974
975 node_data = self.GetItemData(node)
976
977 if isinstance(node_data, gmEMRStructItems.cHealthIssue):
978 self.__expand_issue_node(issue_node = node)
979 return
980
981 if isinstance(node_data, gmEMRStructItems.cEpisode):
982 self.__expand_episode_node(episode_node = node)
983 return
984
985
986 if type(node_data) == type({}):
987 self.__expand_pseudo_issue_node(fake_issue_node = node)
988 return
989
990
991
992
993
995 sel_item = event.GetItem()
996 self.__curr_node = sel_item
997 self.__update_text_for_selected_node()
998 return True
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1152 """Right button clicked: display the popup for the tree"""
1153
1154 node = event.GetItem()
1155 self.SelectItem(node)
1156 self.__curr_node_data = self.GetItemData(node)
1157 self.__curr_node = node
1158
1159 pos = wx.DefaultPosition
1160 if isinstance(self.__curr_node_data, gmEMRStructItems.cHealthIssue):
1161 self.__handle_issue_context(pos=pos)
1162 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEpisode):
1163 self.__handle_episode_context(pos=pos)
1164 elif isinstance(self.__curr_node_data, gmEMRStructItems.cEncounter):
1165 self.__handle_encounter_context(pos=pos)
1166 elif node == self.GetRootItem():
1167 self.__handle_root_context()
1168 elif type(self.__curr_node_data) == type({}):
1169
1170 pass
1171 else:
1172 print("error: unknown node type, no popup menu")
1173 event.Skip()
1174
1175
1177 """Used in sorting items.
1178
1179 -1: 1 < 2
1180 0: 1 = 2
1181 1: 1 > 2
1182 """
1183
1184
1185 if not node1:
1186 _log.debug('invalid node 1')
1187 return 0
1188 if not node2:
1189 _log.debug('invalid node 2')
1190 return 0
1191
1192 if not node1.IsOk():
1193 _log.debug('invalid node 1')
1194 return 0
1195 if not node2.IsOk():
1196 _log.debug('invalid node 2')
1197 return 0
1198
1199 item1 = self.GetItemData(node1)
1200 item2 = self.GetItemData(node2)
1201
1202
1203 if isinstance(item1, type({})):
1204 return -1
1205 if isinstance(item2, type({})):
1206 return 1
1207
1208
1209 if isinstance(item1, gmEMRStructItems.cEncounter):
1210 if item1['started'] == item2['started']:
1211 return 0
1212 if item1['started'] > item2['started']:
1213 return -1
1214 return 1
1215
1216
1217 if isinstance(item1, gmEMRStructItems.cEpisode):
1218
1219 if item1['episode_open']:
1220 return -1
1221 if item2['episode_open']:
1222 return 1
1223 start1 = item1.best_guess_clinical_start_date
1224 start2 = item2.best_guess_clinical_start_date
1225 if start1 == start2:
1226 return 0
1227 if start1 < start2:
1228 return 1
1229 return -1
1230
1231
1232 if isinstance(item1, gmEMRStructItems.cHealthIssue):
1233
1234
1235 if item1['grouping'] is None:
1236 if item2['grouping'] is not None:
1237 return 1
1238
1239
1240 if item1['grouping'] is not None:
1241 if item2['grouping'] is None:
1242 return -1
1243
1244
1245 if (item1['grouping'] is None) and (item2['grouping'] is None):
1246 if item1['description'].lower() < item2['description'].lower():
1247 return -1
1248 if item1['description'].lower() > item2['description'].lower():
1249 return 1
1250 return 0
1251
1252
1253 if item1['grouping'] < item2['grouping']:
1254 return -1
1255
1256 if item1['grouping'] > item2['grouping']:
1257 return 1
1258
1259 if item1['description'].lower() < item2['description'].lower():
1260 return -1
1261
1262 if item1['description'].lower() > item2['description'].lower():
1263 return 1
1264
1265 return 0
1266
1267 _log.error('unknown item type during sorting EMR tree:')
1268 _log.error('item1: %s', type(item1))
1269 _log.error('item2: %s', type(item2))
1270
1271 return 0
1272
1273
1274
1275
1278
1280 if self.__pat == patient:
1281 return
1282 self.__pat = patient
1283 if patient is None:
1284 self.clear_tree()
1285 return
1286 return self.__populate_tree()
1287
1288 patient = property(_get_patient, _set_patient)
1289
1291 return self.__soap_display_mode
1292
1294 if mode not in ['details', 'journal', 'revisions']:
1295 raise ValueError('details display mode must be one of "details", "journal", "revisions"')
1296 if self.__soap_display_mode == mode:
1297 return
1298 self.__soap_display_mode = mode
1299 self.__update_text_for_selected_node()
1300
1301 details_display_mode = property(_get_details_display_mode, _set_details_display_mode)
1302
1303
1304
1305 from Gnumed.wxGladeWidgets import wxgScrolledEMRTreePnl
1306
1316
1317
1318 from Gnumed.wxGladeWidgets import wxgSplittedEMRTreeBrowserPnl
1319
1321 """A splitter window holding an EMR tree.
1322
1323 The left hand side displays a scrollable EMR tree while
1324 on the right details for selected items are displayed.
1325
1326 Expects to be put into a Notebook.
1327 """
1338
1340 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
1341 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
1342 return True
1343
1344
1346 return self.__editing
1347
1349 self.__editing = editing
1350 self.enable_display_mode_selection(enable = not self.__editing)
1351 if self.__editing:
1352 self._BTN_switch_browse_edit.SetLabel(_('&Browse %s') % gmTools.u_ellipsis)
1353 self._PNL_browse.Hide()
1354 self._PNL_visual_soap.Hide()
1355 self._PNL_edit.Show()
1356 else:
1357 self._BTN_switch_browse_edit.SetLabel(_('&New notes %s') % gmTools.u_ellipsis)
1358 self._PNL_edit.Hide()
1359 self._PNL_visual_soap.Show()
1360 self._PNL_browse.Show()
1361 self._PNL_right_side.GetSizer().Layout()
1362
1363 editing = property(_get_editing, _set_editing)
1364
1365
1366
1367
1369 self._pnl_emr_tree._emr_tree.patient = None
1370 self._PNL_edit.patient = None
1371 return True
1372
1373
1375 if self.GetParent().GetCurrentPage() != self:
1376 return True
1377 self.repopulate_ui()
1378 return True
1379
1380
1383
1384
1387
1388
1391
1392
1395
1396
1397
1398
1400 """Fills UI with data."""
1401 pat = gmPerson.gmCurrentPatient()
1402 self._pnl_emr_tree._emr_tree.patient = pat
1403 self._PNL_edit.patient = pat
1404 self._splitter_browser.SetSashPosition(self._splitter_browser.GetSize()[0] // 3, True)
1405
1406 return True
1407
1408
1410 if self.editing:
1411 enable = False
1412 if enable:
1413 self._RBTN_details.Enable(True)
1414 self._RBTN_journal.Enable(True)
1415 self._RBTN_revisions.Enable(True)
1416 return
1417 self._RBTN_details.Enable(False)
1418 self._RBTN_journal.Enable(False)
1419 self._RBTN_revisions.Enable(False)
1420
1421
1423 self._PNL_edit._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_same_problem)
1424
1425
1428
1429
1430 from Gnumed.wxGladeWidgets import wxgEMRJournalPluginPnl
1431
1433
1439
1440
1441
1457
1458
1459
1461 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
1462 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
1463 return True
1464
1465
1466
1467
1469 self._TCTRL_journal.SetValue('')
1470 return True
1471
1472
1474 if self.GetParent().GetCurrentPage() != self:
1475 return True
1476 self.repopulate_ui()
1477 return True
1478
1479
1482
1483
1486
1487
1490
1491
1492 from Gnumed.wxGladeWidgets import wxgEMRListJournalPluginPnl
1493
1495
1506
1507
1508
1509
1511 self._LCTRL_journal.remove_items_safely()
1512 self._TCTRL_details.SetValue('')
1513
1514 if self._RBTN_by_encounter.Value:
1515 order_by = 'encounter_started, pk_episode, src_table, scr, modified_when'
1516
1517 date_col_header = _('Encounter')
1518 date_fields = ['encounter_started', 'modified_when']
1519 elif self._RBTN_by_last_modified.Value:
1520 order_by = 'modified_when, pk_episode, src_table, scr'
1521
1522 date_col_header = _('Modified')
1523 date_fields = ['modified_when']
1524 elif self._RBTN_by_item_time.Value:
1525 order_by = 'clin_when, pk_episode, src_table, scr, modified_when'
1526 date_col_header = _('Clinical time')
1527 date_fields = ['clin_when', 'modified_when']
1528 else:
1529 raise ValueError('invalid EMR journal list sort state')
1530
1531 self._LCTRL_journal.set_columns([date_col_header, '', _('Entry'), _('Who / When')])
1532 self._LCTRL_journal.set_resize_column(3)
1533
1534 journal = gmPerson.gmCurrentPatient().emr.get_as_journal(order_by = order_by)
1535
1536 items = []
1537 data = []
1538 self.__data = {}
1539 prev_date = None
1540 for entry in journal:
1541 if entry['narrative'].strip() == '':
1542 continue
1543 soap_cat = gmSoapDefs.soap_cat2l10n[entry['soap_cat']]
1544 who = '%s (%s)' % (entry['modified_by'], entry['date_modified'])
1545 try:
1546 entry_date = gmDateTime.pydt_strftime(entry[date_fields[0]], '%Y-%m-%d')
1547 except KeyError:
1548 entry_date = gmDateTime.pydt_strftime(entry[date_fields[1]], '%Y-%m-%d')
1549 if entry_date == prev_date:
1550 date2show = ''
1551 else:
1552 date2show = entry_date
1553 prev_date = entry_date
1554 lines = entry['narrative'].strip().split('\n')
1555 line_0 = lines[0].rstrip()
1556 if len(lines) == 1:
1557 delim = gmTools.u_box_horiz_light_3dashes * 10 + gmTools.u_box_T_left
1558 else:
1559 delim = (gmTools.u_box_horiz_light_3dashes * 10) + gmTools.u_box_top_right_arc
1560 entry_line = '%s %s' % (line_0, delim)
1561 items.append([date2show, soap_cat, entry_line, who])
1562 try:
1563 self.__data[entry['src_table']]
1564 except KeyError:
1565 self.__data[entry['src_table']] = {}
1566 try:
1567 self.__data[entry['src_table']][entry['src_pk']]
1568 except KeyError:
1569 self.__data[entry['src_table']][entry['src_pk']] = {}
1570 self.__data[entry['src_table']][entry['src_pk']]['entry'] = entry
1571 self.__data[entry['src_table']][entry['src_pk']]['formatted_instance'] = None
1572 if entry['encounter_started'] is None:
1573 enc_duration = gmTools.u_diameter
1574 else:
1575 enc_duration = '%s - %s' % (
1576 gmDateTime.pydt_strftime(entry['encounter_started'], '%Y %b %d %H:%M'),
1577 gmDateTime.pydt_strftime(entry['encounter_last_affirmed'], '%H:%M')
1578 )
1579 self.__data[entry['src_table']][entry['src_pk']]['formatted_header'] = _(
1580 'Chart entry: %s [#%s in %s]\n'
1581 ' Modified: %s by %s (%s rev %s)\n'
1582 '\n'
1583 'Health issue: %s%s\n'
1584 'Episode: %s%s\n'
1585 'Encounter: %s%s'
1586 ) % (
1587 gmClinicalRecord.format_clin_root_item_type(entry['src_table']),
1588 entry['src_pk'],
1589 entry['src_table'],
1590 entry['date_modified'],
1591 entry['modified_by'],
1592 gmTools.u_arrow2right,
1593 entry['row_version'],
1594 gmTools.coalesce(entry['health_issue'], gmTools.u_diameter, '%s'),
1595 gmTools.bool2subst(entry['issue_active'], ' (' + _('active') + ')', ' (' + _('inactive') + ')', ''),
1596 gmTools.coalesce(entry['episode'], gmTools.u_diameter, '%s'),
1597 gmTools.bool2subst(entry['episode_open'], ' (' + _('open') + ')', ' (' + _('closed') + ')', ''),
1598 enc_duration,
1599 gmTools.coalesce(entry['encounter_l10n_type'], '', ' (%s)'),
1600 )
1601 self.__data[entry['src_table']][entry['src_pk']]['formatted_root_item'] = _(
1602 '%s\n'
1603 '\n'
1604 ' rev %s (%s) by %s in <%s>'
1605 ) % (
1606 entry['narrative'].strip(),
1607 entry['row_version'],
1608 entry['date_modified'],
1609 entry['modified_by'],
1610 entry['src_table']
1611 )
1612 data.append ({
1613 'table': entry['src_table'],
1614 'pk': entry['src_pk']
1615 })
1616 if len(lines) > 1:
1617 lines = lines[1:]
1618 idx = 0
1619 last_line = len(lines)
1620 for entry_line in lines:
1621 idx += 1
1622 if entry_line.strip() == '':
1623 continue
1624 if idx == last_line:
1625 bar = gmTools.u_box_bottom_left_arc
1626 else:
1627 bar = gmTools.u_box_vert_light_4dashes
1628 items.append(['', bar, entry_line.rstrip(), who])
1629 data.append ({
1630 'table': entry['src_table'],
1631 'pk': entry['src_pk']
1632 })
1633
1634 self._LCTRL_journal.set_string_items(items)
1635 self._LCTRL_journal.set_column_widths([wx.LIST_AUTOSIZE, wx.LIST_AUTOSIZE, wx.LIST_AUTOSIZE, wx.LIST_AUTOSIZE_USEHEADER])
1636 self._LCTRL_journal.set_data(data)
1637
1638 self._LCTRL_journal.SetFocus()
1639 return True
1640
1641
1642
1643
1645 gmDispatcher.connect(signal = 'pre_patient_unselection', receiver = self._on_pre_patient_unselection)
1646 gmDispatcher.connect(signal = 'post_patient_selection', receiver = self._on_post_patient_selection)
1647 return True
1648
1649
1650
1651
1657
1658
1660 if self.GetParent().GetCurrentPage() != self:
1661 return True
1662 self.repopulate_ui()
1663 return True
1664
1665
1667
1668 data = self._LCTRL_journal.get_item_data(item_idx = evt.Index)
1669 if self.__data[data['table']][data['pk']]['formatted_instance'] is None:
1670 txt = _(
1671 '%s\n'
1672 '%s\n'
1673 '%s'
1674 ) % (
1675 self.__data[data['table']][data['pk']]['formatted_header'],
1676 gmTools.u_box_horiz_4dashes * 40,
1677 self.__data[data['table']][data['pk']]['formatted_root_item']
1678 )
1679
1680 self._TCTRL_details.SetValue(txt)
1681 self.__load_timer.Stop()
1682 self.__load_timer.Start(oneShot = True)
1683 return
1684
1685 txt = _(
1686 '%s\n'
1687 '%s\n'
1688 '%s'
1689 ) % (
1690 self.__data[data['table']][data['pk']]['formatted_header'],
1691 gmTools.u_box_horiz_4dashes * 40,
1692 self.__data[data['table']][data['pk']]['formatted_instance']
1693 )
1694 self._TCTRL_details.SetValue(txt)
1695
1696
1711
1712
1715
1716
1719
1720
1723
1724
1728
1729
1732
1733
1736
1737
1738
1739
1740
1741
1742
1743
1744 if __name__ == '__main__':
1745
1746 _log.info("starting emr browser...")
1747
1748 try:
1749
1750 patient = gmPersonSearch.ask_for_patient()
1751 if patient is None:
1752 print("No patient. Exiting gracefully...")
1753 sys.exit(0)
1754 gmPatSearchWidgets.set_active_patient(patient = patient)
1755
1756
1757 application = wx.PyWidgetTester(size=(800,600))
1758 emr_browser = cEMRBrowserPanel(application.frame, -1)
1759 emr_browser.refresh_tree()
1760
1761 application.frame.Show(True)
1762 application.MainLoop()
1763
1764
1765 if patient is not None:
1766 try:
1767 patient.cleanup()
1768 except:
1769 print("error cleaning up patient")
1770 except Exception:
1771 _log.exception("unhandled exception caught !")
1772
1773 raise
1774
1775 _log.info("closing emr browser...")
1776
1777
1778