1 """GNUmed narrative handling widgets."""
2
3 __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
4 __license__ = "GPL v2 or later (details at http://www.gnu.org)"
5
6 import sys
7 import logging
8 import os
9 import os.path
10 import time
11 import re as regex
12 import shutil
13
14
15 import wx
16 import wx.lib.expando as wx_expando
17 import wx.lib.agw.supertooltip as agw_stt
18 import wx.lib.statbmp as wx_genstatbmp
19
20
21 if __name__ == '__main__':
22 sys.path.insert(0, '../../')
23
24 from Gnumed.pycommon import gmI18N
25
26 if __name__ == '__main__':
27 gmI18N.activate_locale()
28 gmI18N.install_domain()
29
30 from Gnumed.pycommon import gmDispatcher
31 from Gnumed.pycommon import gmTools
32 from Gnumed.pycommon import gmDateTime
33 from Gnumed.pycommon import gmShellAPI
34 from Gnumed.pycommon import gmPG2
35 from Gnumed.pycommon import gmCfg
36 from Gnumed.pycommon import gmMatchProvider
37
38 from Gnumed.business import gmPerson
39 from Gnumed.business import gmStaff
40 from Gnumed.business import gmEMRStructItems
41 from Gnumed.business import gmClinNarrative
42 from Gnumed.business import gmPraxis
43 from Gnumed.business import gmForms
44 from Gnumed.business import gmDocuments
45 from Gnumed.business import gmPersonSearch
46 from Gnumed.business import gmKeywordExpansion
47
48 from Gnumed.wxpython import gmListWidgets
49 from Gnumed.wxpython import gmEMRStructWidgets
50 from Gnumed.wxpython import gmEncounterWidgets
51 from Gnumed.wxpython import gmRegetMixin
52 from Gnumed.wxpython import gmPhraseWheel
53 from Gnumed.wxpython import gmGuiHelpers
54 from Gnumed.wxpython import gmCfgWidgets
55 from Gnumed.wxpython import gmDocumentWidgets
56 from Gnumed.wxpython import gmKeywordExpansionWidgets
57 from Gnumed.wxpython.gmPatSearchWidgets import set_active_patient
58
59 from Gnumed.exporters import gmPatientExporter
60
61
62 _log = logging.getLogger('gm.ui')
63
64
65
67
68
69 if patient is None:
70 patient = gmPerson.gmCurrentPatient()
71
72 if not patient.connected:
73 gmDispatcher.send(signal = 'statustext', msg = _('Cannot move progress notes. No active patient.'))
74 return False
75
76 if parent is None:
77 parent = wx.GetApp().GetTopWindow()
78
79 emr = patient.get_emr()
80
81 if encounters is None:
82 encs = emr.get_encounters(episodes = episodes)
83 encounters = gmEncounterWidgets.select_encounters (
84 parent = parent,
85 patient = patient,
86 single_selection = False,
87 encounters = encs
88 )
89
90 if encounters is None:
91 return True
92
93 if len(encounters) == 0:
94 return True
95
96 notes = emr.get_clin_narrative (
97 encounters = encounters,
98 episodes = episodes
99 )
100
101
102 if move_all:
103 selected_narr = notes
104 else:
105 selected_narr = gmListWidgets.get_choices_from_list (
106 parent = parent,
107 caption = _('Moving progress notes between encounters ...'),
108 single_selection = False,
109 can_return_empty = True,
110 data = notes,
111 msg = _('\n Select the progress notes to move from the list !\n\n'),
112 columns = [_('when'), _('who'), _('type'), _('entry')],
113 choices = [
114 [ narr['date'].strftime('%x %H:%M'),
115 narr['modified_by'],
116 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
117 narr['narrative'].replace('\n', '/').replace('\r', '/')
118 ] for narr in notes
119 ]
120 )
121
122 if not selected_narr:
123 return True
124
125
126 enc2move2 = gmEncounterWidgets.select_encounters (
127 parent = parent,
128 patient = patient,
129 single_selection = True
130 )
131
132 if not enc2move2:
133 return True
134
135 for narr in selected_narr:
136 narr['pk_encounter'] = enc2move2['pk_encounter']
137 narr.save()
138
139 return True
140
142
143
144 if patient is None:
145 patient = gmPerson.gmCurrentPatient()
146
147 if not patient.connected:
148 gmDispatcher.send(signal = 'statustext', msg = _('Cannot edit progress notes. No active patient.'))
149 return False
150
151 if parent is None:
152 parent = wx.GetApp().GetTopWindow()
153
154 emr = patient.get_emr()
155
156 def delete(item):
157 if item is None:
158 return False
159 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
160 parent,
161 -1,
162 caption = _('Deleting progress note'),
163 question = _(
164 'Are you positively sure you want to delete this\n'
165 'progress note from the medical record ?\n'
166 '\n'
167 'Note that even if you chose to delete the entry it will\n'
168 'still be (invisibly) kept in the audit trail to protect\n'
169 'you from litigation because physical deletion is known\n'
170 'to be unlawful in some jurisdictions.\n'
171 ),
172 button_defs = (
173 {'label': _('Delete'), 'tooltip': _('Yes, delete the progress note.'), 'default': False},
174 {'label': _('Cancel'), 'tooltip': _('No, do NOT delete the progress note.'), 'default': True}
175 )
176 )
177 decision = dlg.ShowModal()
178
179 if decision != wx.ID_YES:
180 return False
181
182 gmClinNarrative.delete_clin_narrative(narrative = item['pk_narrative'])
183 return True
184
185 def edit(item):
186 if item is None:
187 return False
188
189 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
190 parent,
191 -1,
192 title = _('Editing progress note'),
193 msg = _('This is the original progress note:'),
194 data = item.format(left_margin = u' ', fancy = True),
195 text = item['narrative']
196 )
197 decision = dlg.ShowModal()
198
199 if decision != wx.ID_SAVE:
200 return False
201
202 val = dlg.value
203 dlg.Destroy()
204 if val.strip() == u'':
205 return False
206
207 item['narrative'] = val
208 item.save_payload()
209
210 return True
211
212 def refresh(lctrl):
213 notes = emr.get_clin_narrative (
214 encounters = encounters,
215 episodes = episodes,
216 providers = [ gmStaff.gmCurrentProvider()['short_alias'] ]
217 )
218 lctrl.set_string_items(items = [
219 [ narr['date'].strftime('%x %H:%M'),
220 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
221 narr['narrative'].replace('\n', '/').replace('\r', '/')
222 ] for narr in notes
223 ])
224 lctrl.set_data(data = notes)
225
226
227 gmListWidgets.get_choices_from_list (
228 parent = parent,
229 caption = _('Managing progress notes'),
230 msg = _(
231 '\n'
232 ' This list shows the progress notes by %s.\n'
233 '\n'
234 ) % gmStaff.gmCurrentProvider()['short_alias'],
235 columns = [_('when'), _('type'), _('entry')],
236 single_selection = True,
237 can_return_empty = False,
238 edit_callback = edit,
239 delete_callback = delete,
240 refresh_callback = refresh
241 )
242
244
245 if parent is None:
246 parent = wx.GetApp().GetTopWindow()
247
248 search_term_dlg = wx.TextEntryDialog (
249 parent = parent,
250 message = _('Enter (regex) term to search for across all EMRs:'),
251 caption = _('Text search across all EMRs'),
252 style = wx.OK | wx.CANCEL | wx.CENTRE
253 )
254 result = search_term_dlg.ShowModal()
255
256 if result != wx.ID_OK:
257 return
258
259 wx.BeginBusyCursor()
260 search_term = search_term_dlg.GetValue()
261 search_term_dlg.Destroy()
262 results = gmClinNarrative.search_text_across_emrs(search_term = search_term)
263 wx.EndBusyCursor()
264
265 if len(results) == 0:
266 gmGuiHelpers.gm_show_info (
267 _(
268 'Nothing found for search term:\n'
269 ' "%s"'
270 ) % search_term,
271 _('Search results')
272 )
273 return
274
275 items = [ [
276 gmPerson.cIdentity(aPK_obj = r['pk_patient'])['description_gender'],
277 r['narrative'],
278 r['src_table']
279 ] for r in results ]
280
281 selected_patient = gmListWidgets.get_choices_from_list (
282 parent = parent,
283 caption = _('Search results for [%s]') % search_term,
284 choices = items,
285 columns = [_('Patient'), _('Match'), _('Match location')],
286 data = [ r['pk_patient'] for r in results ],
287 single_selection = True,
288 can_return_empty = False
289 )
290
291 if selected_patient is None:
292 return
293
294 wx.CallAfter(set_active_patient, patient = gmPerson.cIdentity(aPK_obj = selected_patient))
295
297
298
299 if patient is None:
300 patient = gmPerson.gmCurrentPatient()
301
302 if not patient.connected:
303 gmDispatcher.send(signal = 'statustext', msg = _('Cannot search EMR. No active patient.'))
304 return False
305
306 if parent is None:
307 parent = wx.GetApp().GetTopWindow()
308
309 search_term_dlg = wx.TextEntryDialog (
310 parent = parent,
311 message = _('Enter search term:'),
312 caption = _('Text search of entire EMR of active patient'),
313 style = wx.OK | wx.CANCEL | wx.CENTRE
314 )
315 result = search_term_dlg.ShowModal()
316
317 if result != wx.ID_OK:
318 search_term_dlg.Destroy()
319 return False
320
321 wx.BeginBusyCursor()
322 val = search_term_dlg.GetValue()
323 search_term_dlg.Destroy()
324 emr = patient.get_emr()
325 rows = emr.search_narrative_simple(val)
326 wx.EndBusyCursor()
327
328 if len(rows) == 0:
329 gmGuiHelpers.gm_show_info (
330 _(
331 'Nothing found for search term:\n'
332 ' "%s"'
333 ) % val,
334 _('Search results')
335 )
336 return True
337
338 txt = u''
339 for row in rows:
340 txt += u'%s: %s\n' % (
341 row['soap_cat'],
342 row['narrative']
343 )
344
345 txt += u' %s: %s - %s %s\n' % (
346 _('Encounter'),
347 row['encounter_started'].strftime('%x %H:%M'),
348 row['encounter_ended'].strftime('%H:%M'),
349 row['encounter_type']
350 )
351 txt += u' %s: %s\n' % (
352 _('Episode'),
353 row['episode']
354 )
355 txt += u' %s: %s\n\n' % (
356 _('Health issue'),
357 row['health_issue']
358 )
359
360 msg = _(
361 'Search term was: "%s"\n'
362 '\n'
363 'Search results:\n\n'
364 '%s\n'
365 ) % (val, txt)
366
367 dlg = wx.MessageDialog (
368 parent = parent,
369 message = msg,
370 caption = _('Search results for [%s]') % val,
371 style = wx.OK | wx.STAY_ON_TOP
372 )
373 dlg.ShowModal()
374 dlg.Destroy()
375
376 return True
377
379
380
381 pat = gmPerson.gmCurrentPatient()
382 if not pat.connected:
383 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR for Medistar. No active patient.'))
384 return False
385
386 if encounter is None:
387 encounter = pat.get_emr().active_encounter
388
389 if parent is None:
390 parent = wx.GetApp().GetTopWindow()
391
392
393 aWildcard = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files"))
394
395 aDefDir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed')))
396
397 fname = '%s-%s-%s-%s-%s.txt' % (
398 'Medistar-MD',
399 time.strftime('%Y-%m-%d',time.localtime()),
400 pat['lastnames'].replace(' ', '-'),
401 pat['firstnames'].replace(' ', '_'),
402 pat.get_formatted_dob(format = '%Y-%m-%d')
403 )
404 dlg = wx.FileDialog (
405 parent = parent,
406 message = _("Save EMR extract for MEDISTAR import as..."),
407 defaultDir = aDefDir,
408 defaultFile = fname,
409 wildcard = aWildcard,
410 style = wx.SAVE
411 )
412 choice = dlg.ShowModal()
413 fname = dlg.GetPath()
414 dlg.Destroy()
415 if choice != wx.ID_OK:
416 return False
417
418 wx.BeginBusyCursor()
419 _log.debug('exporting encounter for medistar import to [%s]', fname)
420 exporter = gmPatientExporter.cMedistarSOAPExporter()
421 successful, fname = exporter.export_to_file (
422 filename = fname,
423 encounter = encounter,
424 soap_cats = u'soapu',
425 export_to_import_file = True
426 )
427 if not successful:
428 gmGuiHelpers.gm_show_error (
429 _('Error exporting progress notes for MEDISTAR import.'),
430 _('MEDISTAR progress notes export')
431 )
432 wx.EndBusyCursor()
433 return False
434
435 gmDispatcher.send(signal = 'statustext', msg = _('Successfully exported progress notes into file [%s] for Medistar import.') % fname, beep=False)
436
437 wx.EndBusyCursor()
438 return True
439
440
442
443 pat = gmPerson.gmCurrentPatient()
444 emr = pat.get_emr()
445
446 if parent is None:
447 parent = wx.GetApp().GetTopWindow()
448
449 if soap_cats is None:
450 soap_cats = u'soapu'
451 soap_cats = list(soap_cats)
452 i18n_soap_cats = [ gmClinNarrative.soap_cat2l10n[cat].upper() for cat in soap_cats ]
453
454 if msg is None:
455 msg = _('Pick the [%s] narrative you want to use.') % u'/'.join(i18n_soap_cats)
456
457
458 def get_tooltip(soap):
459 return soap.format(fancy = True, width = 60)
460
461 def refresh(lctrl):
462 lctrl.secondary_sort_column = 0
463 soap = emr.get_clin_narrative(soap_cats = soap_cats)
464 lctrl.set_string_items ([ [
465 gmDateTime.pydt_strftime(s['date'], '%Y %m %d'),
466 s['modified_by'],
467 gmClinNarrative.soap_cat2l10n[s['soap_cat']],
468 s['narrative'],
469 s['episode'],
470 s['health_issue']
471 ] for s in soap ])
472 lctrl.set_data(soap)
473
474 return gmListWidgets.get_choices_from_list (
475 parent = parent,
476 msg = msg,
477 caption = _('Picking [%s] narrative') % (u'/'.join(i18n_soap_cats)),
478 columns = [_('When'), _('Who'), _('Type'), _('Entry'), _('Episode'), _('Issue')],
479 single_selection = False,
480 can_return_empty = False,
481 refresh_callback = refresh,
482 list_tooltip_callback = get_tooltip
483 )
484
485
487
488 pat = gmPerson.gmCurrentPatient()
489 emr = pat.get_emr()
490
491
492
493
494
495
496
497 if parent is None:
498 parent = wx.GetApp().GetTopWindow()
499
500 if soap_cats is None:
501 soap_cats = u'soapu'
502 soap_cats = list(soap_cats)
503 i18n_soap_cats = [ gmClinNarrative.soap_cat2l10n[cat].upper() for cat in soap_cats ]
504
505 selected_soap = {}
506
507
508
509 def get_soap_tooltip(soap):
510 return soap.format(fancy = True, width = 60)
511
512 def pick_soap_from_issue(issue):
513
514 if issue is None:
515 return False
516
517 narr_for_issue = emr.get_clin_narrative(issues = [issue['pk_health_issue']], soap_cats = soap_cats)
518
519 if len(narr_for_issue) == 0:
520 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for this health issue.'))
521 return True
522
523 selected_narr = gmListWidgets.get_choices_from_list (
524 parent = parent,
525 msg = _('Pick the [%s] narrative you want to include in the report.') % u'/'.join(i18n_soap_cats),
526 caption = _('Picking [%s] from %s%s%s') % (
527 u'/'.join(i18n_soap_cats),
528 gmTools.u_left_double_angle_quote,
529 issue['description'],
530 gmTools.u_right_double_angle_quote
531 ),
532 columns = [_('When'), _('Who'), _('Type'), _('Entry')],
533 choices = [ [
534 gmDateTime.pydt_strftime(narr['date'], '%Y %b %d %H:%M', accuracy = gmDateTime.acc_minutes),
535 narr['modified_by'],
536 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
537 narr['narrative'].replace('\n', '//').replace('\r', '//')
538 ] for narr in narr_for_issue ],
539 data = narr_for_issue,
540
541
542 single_selection = False,
543 can_return_empty = False,
544 list_tooltip_callback = get_soap_tooltip
545 )
546
547 if selected_narr is None:
548 return True
549
550 for narr in selected_narr:
551 selected_soap[narr['pk_narrative']] = narr
552
553 return True
554
555 def edit_issue(issue):
556 return gmEMRStructWidgets.edit_health_issue(parent = parent, issue = issue)
557
558 def refresh_issues(lctrl):
559
560 issues = emr.health_issues
561 lctrl.set_string_items ([ [
562 gmTools.bool2subst(i['is_confidential'], _('!! CONFIDENTIAL !!'), u''),
563 i['description'],
564 gmTools.bool2subst(i['is_active'], _('active'), _('inactive'))
565 ] for i in issues
566 ])
567 lctrl.set_data(issues)
568
569 def get_issue_tooltip(issue):
570 return issue.format (
571 patient = pat,
572 with_encounters = False,
573 with_medications = False,
574 with_hospital_stays = False,
575 with_procedures = False,
576 with_family_history = False,
577 with_documents = False,
578 with_tests = False,
579 with_vaccinations = False
580 )
581
582
583
584 issues_picked_from = gmListWidgets.get_choices_from_list (
585 parent = parent,
586 msg = _('\n Select the issue you want to report on.'),
587 caption = _('Picking [%s] from health issues') % u'/'.join(i18n_soap_cats),
588 columns = [_('Privacy'), _('Issue'), _('Status')],
589 edit_callback = edit_issue,
590 refresh_callback = refresh_issues,
591 single_selection = True,
592 can_return_empty = True,
593 ignore_OK_button = False,
594 left_extra_button = (
595 _('&Pick notes'),
596 _('Pick [%s] entries from selected health issue') % u'/'.join(i18n_soap_cats),
597 pick_soap_from_issue
598 ),
599 list_tooltip_callback = get_issue_tooltip
600 )
601
602 if issues_picked_from is None:
603 return []
604
605 return selected_soap.values()
606
607
608
609
610
611
612
613
615
616 pat = gmPerson.gmCurrentPatient()
617 emr = pat.get_emr()
618
619 all_epis = [ epi for epi in emr.get_episodes(order_by = u'description') if epi.has_narrative ]
620 if len(all_epis) == 0:
621 gmDispatcher.send(signal = 'statustext', msg = _('No episodes with progress notes found.'))
622 return []
623
624 if parent is None:
625 parent = wx.GetApp().GetTopWindow()
626
627 if soap_cats is None:
628 soap_cats = u'soapu'
629 soap_cats = list(soap_cats)
630 i18n_soap_cats = [ gmClinNarrative.soap_cat2l10n[cat].upper() for cat in soap_cats ]
631
632 selected_soap = {}
633
634
635
636 def get_soap_tooltip(soap):
637 return soap.format(fancy = True, width = 60)
638
639 def pick_soap_from_episode(episode):
640
641 if episode is None:
642 return False
643
644 narr_for_epi = emr.get_clin_narrative(episodes = [episode['pk_episode']], soap_cats = soap_cats)
645
646 if len(narr_for_epi) == 0:
647 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episode.'))
648 return True
649
650 selected_narr = gmListWidgets.get_choices_from_list (
651 parent = parent,
652 msg = _('Pick the [%s] narrative you want to include in the report.') % u'/'.join(i18n_soap_cats),
653 caption = _('Picking [%s] from %s%s%s') % (
654 u'/'.join(i18n_soap_cats),
655 gmTools.u_left_double_angle_quote,
656 episode['description'],
657 gmTools.u_right_double_angle_quote
658 ),
659 columns = [_('When'), _('Who'), _('Type'), _('Entry')],
660 choices = [ [
661 gmDateTime.pydt_strftime(narr['date'], '%Y %b %d %H:%M', accuracy = gmDateTime.acc_minutes),
662 narr['modified_by'],
663 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
664 narr['narrative'].replace('\n', '//').replace('\r', '//')
665 ] for narr in narr_for_epi ],
666 data = narr_for_epi,
667
668
669 single_selection = False,
670 can_return_empty = False,
671 list_tooltip_callback = get_soap_tooltip
672 )
673
674 if selected_narr is None:
675 return True
676
677 for narr in selected_narr:
678 selected_soap[narr['pk_narrative']] = narr
679
680 return True
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697 def edit_episode(episode):
698 return gmEMRStructWidgets.edit_episode(parent = parent, episode = episode)
699
700 def refresh_episodes(lctrl):
701 all_epis = [ epi for epi in emr.get_episodes(order_by = u'description') if epi.has_narrative ]
702 lctrl.set_string_items ([ [
703 u'%s%s' % (e['description'], gmTools.coalesce(e['health_issue'], u'', u' (%s)')),
704 gmTools.bool2subst(e['episode_open'], _('open'), _('closed'))
705 ] for e in all_epis
706 ])
707 lctrl.set_data(all_epis)
708
709 def get_episode_tooltip(episode):
710 return episode.format (
711 patient = pat,
712 with_encounters = False,
713 with_documents = False,
714 with_hospital_stays = False,
715 with_procedures = False,
716 with_family_history = False,
717 with_tests = False,
718 with_vaccinations = False
719 )
720
721
722
723 epis_picked_from = gmListWidgets.get_choices_from_list (
724 parent = parent,
725 msg = _('\n Select the episode you want to report on.'),
726 caption = _('Picking [%s] from episodes') % u'/'.join(i18n_soap_cats),
727 columns = [_('Episode'), _('Status')],
728 edit_callback = edit_episode,
729 refresh_callback = refresh_episodes,
730 single_selection = True,
731 can_return_empty = True,
732 ignore_OK_button = False,
733 left_extra_button = (
734 _('&Pick notes'),
735 _('Pick [%s] entries from selected episode') % u'/'.join(i18n_soap_cats),
736 pick_soap_from_episode
737 ),
738 list_tooltip_callback = get_episode_tooltip
739 )
740
741 if epis_picked_from is None:
742 return []
743
744 return selected_soap.values()
745
746
747
748
749
750
751
752
754 """soap_cats needs to be a list"""
755
756 pat = gmPerson.gmCurrentPatient()
757 emr = pat.get_emr()
758
759 if parent is None:
760 parent = wx.GetApp().GetTopWindow()
761
762 selected_soap = {}
763 selected_issue_pks = []
764 selected_episode_pks = []
765 selected_narrative_pks = []
766
767 while 1:
768
769 all_issues = emr.get_health_issues()
770 all_issues.insert(0, gmEMRStructItems.get_dummy_health_issue())
771 dlg = gmEMRStructWidgets.cIssueListSelectorDlg (
772 parent = parent,
773 id = -1,
774 issues = all_issues,
775 msg = _('\n In the list below mark the health issues you want to report on.\n')
776 )
777 selection_idxs = []
778 for idx in range(len(all_issues)):
779 if all_issues[idx]['pk_health_issue'] in selected_issue_pks:
780 selection_idxs.append(idx)
781 if len(selection_idxs) != 0:
782 dlg.set_selections(selections = selection_idxs)
783 btn_pressed = dlg.ShowModal()
784 selected_issues = dlg.get_selected_item_data()
785 dlg.Destroy()
786
787 if btn_pressed == wx.ID_CANCEL:
788 return selected_soap.values()
789
790 selected_issue_pks = [ i['pk_health_issue'] for i in selected_issues ]
791
792 while 1:
793
794 all_epis = emr.get_episodes(issues = selected_issue_pks)
795
796 if len(all_epis) == 0:
797 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.'))
798 break
799
800 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg (
801 parent = parent,
802 id = -1,
803 episodes = all_epis,
804 msg = _(
805 '\n These are the episodes known for the health issues just selected.\n\n'
806 ' Now, mark the the episodes you want to report on.\n'
807 )
808 )
809 selection_idxs = []
810 for idx in range(len(all_epis)):
811 if all_epis[idx]['pk_episode'] in selected_episode_pks:
812 selection_idxs.append(idx)
813 if len(selection_idxs) != 0:
814 dlg.set_selections(selections = selection_idxs)
815 btn_pressed = dlg.ShowModal()
816 selected_epis = dlg.get_selected_item_data()
817 dlg.Destroy()
818
819 if btn_pressed == wx.ID_CANCEL:
820 break
821
822 selected_episode_pks = [ i['pk_episode'] for i in selected_epis ]
823
824
825 all_narr = emr.get_clin_narrative(episodes = selected_episode_pks, soap_cats = soap_cats)
826
827 if len(all_narr) == 0:
828 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episodes.'))
829 continue
830
831 dlg = cNarrativeListSelectorDlg (
832 parent = parent,
833 id = -1,
834 narrative = all_narr,
835 msg = _(
836 '\n This is the narrative (type %s) for the chosen episodes.\n\n'
837 ' Now, mark the entries you want to include in your report.\n'
838 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soapu')) ])
839 )
840 selection_idxs = []
841 for idx in range(len(all_narr)):
842 if all_narr[idx]['pk_narrative'] in selected_narrative_pks:
843 selection_idxs.append(idx)
844 if len(selection_idxs) != 0:
845 dlg.set_selections(selections = selection_idxs)
846 btn_pressed = dlg.ShowModal()
847 selected_narr = dlg.get_selected_item_data()
848 dlg.Destroy()
849
850 if btn_pressed == wx.ID_CANCEL:
851 continue
852
853 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ]
854 for narr in selected_narr:
855 selected_soap[narr['pk_narrative']] = narr
856
858
860
861 narrative = kwargs['narrative']
862 del kwargs['narrative']
863
864 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs)
865
866 self.SetTitle(_('Select the narrative you are interested in ...'))
867
868 self._LCTRL_items.set_columns([_('when'), _('who'), _('type'), _('entry')])
869
870 self._LCTRL_items.set_string_items (
871 items = [ [narr['date'].strftime('%x %H:%M'), narr['modified_by'], gmClinNarrative.soap_cat2l10n[narr['soap_cat']], narr['narrative'].replace('\n', '/').replace('\r', '/')] for narr in narrative ]
872 )
873 self._LCTRL_items.set_column_widths()
874 self._LCTRL_items.set_data(data = narrative)
875
876 from Gnumed.wxGladeWidgets import wxgMoveNarrativeDlg
877
879
881
882 self.encounter = kwargs['encounter']
883 self.source_episode = kwargs['episode']
884 del kwargs['encounter']
885 del kwargs['episode']
886
887 wxgMoveNarrativeDlg.wxgMoveNarrativeDlg.__init__(self, *args, **kwargs)
888
889 self.LBL_source_episode.SetLabel(u'%s%s' % (self.source_episode['description'], gmTools.coalesce(self.source_episode['health_issue'], u'', u' (%s)')))
890 self.LBL_encounter.SetLabel('%s: %s %s - %s' % (
891 gmDateTime.pydt_strftime(self.encounter['started'], '%Y %b %d'),
892 self.encounter['l10n_type'],
893 gmDateTime.pydt_strftime(self.encounter['started'], '%H:%M'),
894 gmDateTime.pydt_strftime(self.encounter['last_affirmed'], '%H:%M')
895 ))
896 pat = gmPerson.gmCurrentPatient()
897 emr = pat.get_emr()
898 narr = emr.get_clin_narrative(episodes=[self.source_episode['pk_episode']], encounters=[self.encounter['pk_encounter']])
899 if len(narr) == 0:
900 narr = [{'narrative': _('There is no narrative for this episode in this encounter.')}]
901 self.LBL_narrative.SetLabel(u'\n'.join([n['narrative'] for n in narr]))
902
903
925
926 from Gnumed.wxGladeWidgets import wxgSoapPluginPnl
927
928 -class cSoapPluginPnl(wxgSoapPluginPnl.wxgSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
929 """A panel for in-context editing of progress notes.
930
931 Expects to be used as a notebook page.
932
933 Left hand side:
934 - problem list (health issues and active episodes)
935 - previous notes
936
937 Right hand side:
938 - panel handling
939 - encounter details fields
940 - notebook with progress note editors
941 - visual progress notes
942
943 Listens to patient change signals, thus acts on the current patient.
944 """
954
955
956
958 self._LCTRL_active_problems.set_columns([_('Last'), _('Problem'), _('In health issue')])
959 self._LCTRL_active_problems.set_string_items()
960
961 self._splitter_main.SetSashGravity(0.5)
962 self._splitter_left.SetSashGravity(0.5)
963
964 splitter_size = self._splitter_main.GetSizeTuple()[0]
965 self._splitter_main.SetSashPosition(splitter_size * 3 / 10, True)
966
967 splitter_size = self._splitter_left.GetSizeTuple()[1]
968 self._splitter_left.SetSashPosition(splitter_size * 6 / 20, True)
969
971 """Clear all information from input panel."""
972
973 self._LCTRL_active_problems.set_string_items()
974
975 self._TCTRL_recent_notes.SetValue(u'')
976 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on selected problem'))
977
978 self._PNL_editors.patient = None
979
981 """Update health problems list."""
982
983 self._LCTRL_active_problems.set_string_items()
984
985 emr = self.__pat.get_emr()
986 problems = emr.get_problems (
987 include_closed_episodes = self._CHBOX_show_closed_episodes.IsChecked(),
988 include_irrelevant_issues = self._CHBOX_irrelevant_issues.IsChecked()
989 )
990
991 list_items = []
992 active_problems = []
993 for problem in problems:
994 if not problem['problem_active']:
995 if not problem['is_potential_problem']:
996 continue
997
998 active_problems.append(problem)
999
1000 if problem['type'] == 'issue':
1001 issue = emr.problem2issue(problem)
1002 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue'])
1003 if last_encounter is None:
1004 last = issue['modified_when'].strftime('%m/%Y')
1005 else:
1006 last = last_encounter['last_affirmed'].strftime('%m/%Y')
1007
1008 list_items.append([last, problem['problem'], gmTools.u_left_arrow_with_tail])
1009
1010 elif problem['type'] == 'episode':
1011 epi = emr.problem2episode(problem)
1012 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode'])
1013 if last_encounter is None:
1014 last = epi['episode_modified_when'].strftime('%m/%Y')
1015 else:
1016 last = last_encounter['last_affirmed'].strftime('%m/%Y')
1017
1018 list_items.append ([
1019 last,
1020 problem['problem'],
1021 gmTools.coalesce(initial = epi['health_issue'], instead = u'?')
1022 ])
1023
1024 self._LCTRL_active_problems.set_string_items(items = list_items)
1025 self._LCTRL_active_problems.set_column_widths()
1026 self._LCTRL_active_problems.set_data(data = active_problems)
1027
1028 showing_potential_problems = (
1029 self._CHBOX_show_closed_episodes.IsChecked()
1030 or
1031 self._CHBOX_irrelevant_issues.IsChecked()
1032 )
1033 if showing_potential_problems:
1034 self._SZR_problem_list_staticbox.SetLabel(_('%s (active+potential) problems') % len(list_items))
1035 else:
1036 self._SZR_problem_list_staticbox.SetLabel(_('%s active problems') % len(list_items))
1037
1038 return True
1039
1041 soap = u''
1042 emr = self.__pat.get_emr()
1043 prev_enc = emr.get_last_but_one_encounter(issue_id = problem['pk_health_issue'])
1044 if prev_enc is not None:
1045 soap += prev_enc.format (
1046 issues = [ problem['pk_health_issue'] ],
1047 with_soap = True,
1048 with_docs = fancy,
1049 with_tests = fancy,
1050 patient = self.__pat,
1051 fancy_header = False,
1052 with_rfe_aoe = True
1053 )
1054
1055 tmp = emr.active_encounter.format_soap (
1056 soap_cats = 'soapu',
1057 emr = emr,
1058 issues = [ problem['pk_health_issue'] ],
1059 )
1060 if len(tmp) > 0:
1061 soap += _('Current encounter:') + u'\n'
1062 soap += u'\n'.join(tmp) + u'\n'
1063
1064 if problem['summary'] is not None:
1065 soap += u'\n-- %s ----------\n%s' % (
1066 _('Cumulative summary'),
1067 gmTools.wrap (
1068 text = problem['summary'],
1069 width = 45,
1070 initial_indent = u' ',
1071 subsequent_indent = u' '
1072 ).strip('\n')
1073 )
1074
1075 return soap
1076
1078 soap = u''
1079 emr = self.__pat.get_emr()
1080 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_episode'])
1081 if prev_enc is not None:
1082 soap += prev_enc.format (
1083 episodes = [ problem['pk_episode'] ],
1084 with_soap = True,
1085 with_docs = fancy,
1086 with_tests = fancy,
1087 patient = self.__pat,
1088 fancy_header = False,
1089 with_rfe_aoe = True
1090 )
1091 else:
1092 if problem['pk_health_issue'] is not None:
1093 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_health_issue'])
1094 if prev_enc is not None:
1095 soap += prev_enc.format (
1096 with_soap = True,
1097 with_docs = fancy,
1098 with_tests = fancy,
1099 patient = self.__pat,
1100 issues = [ problem['pk_health_issue'] ],
1101 fancy_header = False,
1102 with_rfe_aoe = True
1103 )
1104
1105 tmp = emr.active_encounter.format_soap (
1106 soap_cats = 'soapu',
1107 emr = emr,
1108 issues = [ problem['pk_health_issue'] ],
1109 )
1110 if len(tmp) > 0:
1111 soap += _('Current encounter:') + u'\n'
1112 soap += u'\n'.join(tmp) + u'\n'
1113
1114 if problem['summary'] is not None:
1115 soap += u'\n-- %s ----------\n%s' % (
1116 _('Cumulative summary'),
1117 gmTools.wrap (
1118 text = problem['summary'],
1119 width = 45,
1120 initial_indent = u' ',
1121 subsequent_indent = u' '
1122 ).strip('\n')
1123 )
1124
1125 return soap
1126
1128 """This refreshes the recent-notes part."""
1129
1130 if problem is None:
1131 caption = u'<?>'
1132 txt = u''
1133 elif problem['type'] == u'issue':
1134 caption = problem['problem'][:35]
1135 txt = self.__get_info_for_issue_problem(problem = problem, fancy = not self._RBTN_notes_only.GetValue())
1136 elif problem['type'] == u'episode':
1137 caption = problem['problem'][:35]
1138 txt = self.__get_info_for_episode_problem(problem = problem, fancy = not self._RBTN_notes_only.GetValue())
1139
1140 self._TCTRL_recent_notes.SetValue(txt)
1141 self._TCTRL_recent_notes.ShowPosition(self._TCTRL_recent_notes.GetLastPosition())
1142 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent info on %s%s%s') % (
1143 gmTools.u_left_double_angle_quote,
1144 caption,
1145 gmTools.u_right_double_angle_quote
1146 ))
1147
1148 self._TCTRL_recent_notes.Refresh()
1149
1150 return True
1151
1152
1153
1155 """Configure enabled event signals."""
1156
1157 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1158 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1159 gmDispatcher.connect(signal = u'clin.episode_mod_db', receiver = self._on_episode_issue_mod_db)
1160 gmDispatcher.connect(signal = u'clin.health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
1161 gmDispatcher.connect(signal = u'clin.episode_code_mod_db', receiver = self._on_episode_issue_mod_db)
1162
1164 wx.CallAfter(self.__on_pre_patient_selection)
1165
1167 self.__reset_ui_content()
1168
1170 wx.CallAfter(self.__on_post_patient_selection)
1171
1173 self._schedule_data_reget()
1174 self._PNL_editors.patient = self.__pat
1175
1177 wx.CallAfter(self._schedule_data_reget)
1178
1179
1180
1182 """Show related note at the bottom."""
1183 pass
1184
1196
1198 """Show related note at the bottom."""
1199 self.__refresh_recent_notes (
1200 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1201 )
1202
1204 """Open progress note editor for this problem.
1205 """
1206 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1207 if problem is None:
1208 return True
1209
1210 dbcfg = gmCfg.cCfgSQL()
1211 allow_duplicate_editors = bool(dbcfg.get2 (
1212 option = u'horstspace.soap_editor.allow_same_episode_multiple_times',
1213 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
1214 bias = u'user',
1215 default = False
1216 ))
1217 if self._PNL_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors):
1218 return True
1219
1220 gmGuiHelpers.gm_show_error (
1221 aMessage = _(
1222 'Cannot open progress note editor for\n\n'
1223 '[%s].\n\n'
1224 ) % problem['problem'],
1225 aTitle = _('opening progress note editor')
1226 )
1227 return False
1228
1230 self.__refresh_problem_list()
1231
1233 self.__refresh_problem_list()
1234
1235
1236
1238 self.__refresh_recent_notes (
1239 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1240 )
1241
1243 self.__refresh_recent_notes (
1244 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1245 )
1246
1247
1248
1249
1250
1251
1252
1254 self.__refresh_problem_list()
1255 return True
1256
1257
1258 from Gnumed.wxGladeWidgets import wxgFancySoapEditorPnl
1259
1261 """A panel holding everything needed to edit
1262
1263 - encounter metadata
1264 - textual progress notes
1265 - visual progress notes
1266
1267 in context. Does NOT act on the current patient.
1268 """
1276
1277
1278
1279 - def add_editor(self, problem=None, allow_same_problem=False):
1280 return self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_same_problem)
1281
1284
1286
1287
1288 self.__pat = patient
1289 self.__refresh_encounter()
1290 self.__refresh_soap_notebook()
1291
1292 patient = property(_get_patient, _set_patient)
1293
1295
1296 if self.__pat is None:
1297 return True
1298
1299 if not self.__encounter_valid_for_save():
1300 return False
1301
1302 enc = self.__pat.emr.active_encounter
1303
1304 rfe = self._TCTRL_rfe.GetValue().strip()
1305 if len(rfe) == 0:
1306 enc['reason_for_encounter'] = None
1307 else:
1308 enc['reason_for_encounter'] = rfe
1309 aoe = self._TCTRL_aoe.GetValue().strip()
1310 if len(aoe) == 0:
1311 enc['assessment_of_encounter'] = None
1312 else:
1313 enc['assessment_of_encounter'] = aoe
1314
1315 enc.save_payload()
1316
1317 enc.generic_codes_rfe = [ c['data'] for c in self._PRW_rfe_codes.GetData() ]
1318 enc.generic_codes_aoe = [ c['data'] for c in self._PRW_aoe_codes.GetData() ]
1319
1320 return True
1321
1322
1323
1325 self._NB_soap_editors.MoveAfterInTabOrder(self._PRW_aoe_codes)
1326
1328 self._NB_soap_editors.DeleteAllPages()
1329 self._NB_soap_editors.add_editor()
1330
1355
1357 self._TCTRL_rfe.SetValue(u'')
1358 self._PRW_rfe_codes.SetText(suppress_smarts = True)
1359 self._TCTRL_aoe.SetValue(u'')
1360 self._PRW_aoe_codes.SetText(suppress_smarts = True)
1361
1384
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1410
1411
1412
1414 """Configure enabled event signals."""
1415
1416 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
1417
1418
1419 gmDispatcher.connect(signal = u'blobs.doc_med_mod_db', receiver = self._on_doc_mod_db)
1420 gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified)
1421 gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_switched)
1422 gmDispatcher.connect(signal = u'clin.rfe_code_mod_db', receiver = self._on_encounter_code_modified)
1423 gmDispatcher.connect(signal = u'clin.aoe_code_mod_db', receiver = self._on_encounter_code_modified)
1424
1426 """Another patient is about to be activated.
1427
1428 Patient change will not proceed before this returns True.
1429 """
1430
1431
1432 if self.__pat is None:
1433 return True
1434 return self._NB_soap_editors.warn_on_unsaved_soap()
1435
1437 """The client is about to (be) shut down.
1438
1439 Shutdown will not proceed before this returns.
1440 """
1441 if self.__pat is None:
1442 return True
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458 saved = self._NB_soap_editors.save_all_editors (
1459 emr = self.__pat.emr,
1460 episode_name_candidates = [
1461 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
1462 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'')
1463 ]
1464 )
1465 if not saved:
1466 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
1467 return True
1468
1470 wx.CallAfter(self.__refresh_current_editor)
1471
1473 wx.CallAfter(self.__on_encounter_code_modified)
1474
1478
1480 wx.CallAfter(self.__refresh_encounter)
1481
1483 wx.CallAfter(self.__refresh_encounter)
1484
1485
1486
1490
1494
1498
1508
1528
1532
1533
1534
1538
1539
1540
1549
1561
1562
1753
1754
1755 from Gnumed.wxGladeWidgets import wxgSoapNoteExpandoEditAreaPnl
1756
1758 """An Edit Area like panel for entering progress notes.
1759
1760 Subjective: Codes:
1761 expando text ctrl
1762 Objective: Codes:
1763 expando text ctrl
1764 Assessment: Codes:
1765 expando text ctrl
1766 Plan: Codes:
1767 expando text ctrl
1768 visual progress notes
1769 panel with images
1770 Episode synopsis: Codes:
1771 text ctrl
1772
1773 - knows the problem this edit area is about
1774 - can deal with issue or episode type problems
1775 """
1776
1778
1779 try:
1780 self.problem = kwargs['problem']
1781 del kwargs['problem']
1782 except KeyError:
1783 self.problem = None
1784
1785 wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl.__init__(self, *args, **kwargs)
1786
1787 self.soap_fields = [
1788 self._TCTRL_Soap,
1789 self._TCTRL_sOap,
1790 self._TCTRL_soAp,
1791 self._TCTRL_soaP
1792 ]
1793
1794 self.__init_ui()
1795 self.__register_interests()
1796
1803
1807
1809 self._TCTRL_episode_summary.SetValue(u'')
1810 self._PRW_episode_codes.SetText(u'', self._PRW_episode_codes.list2data_dict([]))
1811 self._LBL_summary.SetLabel(_('Episode synopsis'))
1812
1813
1814 if self.problem is None:
1815 return
1816
1817
1818 if self.problem['type'] == u'issue':
1819 return
1820
1821
1822 caption = _(u'Synopsis (%s)') % (
1823 gmDateTime.pydt_strftime (
1824 self.problem['modified_when'],
1825 format = '%B %Y',
1826 accuracy = gmDateTime.acc_days
1827 )
1828 )
1829 self._LBL_summary.SetLabel(caption)
1830
1831 if self.problem['summary'] is not None:
1832 self._TCTRL_episode_summary.SetValue(self.problem['summary'].strip())
1833
1834 val, data = self._PRW_episode_codes.generic_linked_codes2item_dict(self.problem.generic_codes)
1835 self._PRW_episode_codes.SetText(val, data)
1836
1856
1858 for field in self.soap_fields:
1859 field.SetValue(u'')
1860 self._TCTRL_episode_summary.SetValue(u'')
1861 self._LBL_summary.SetLabel(_('Episode synopsis'))
1862 self._PRW_episode_codes.SetText(u'', self._PRW_episode_codes.list2data_dict([]))
1863 self._PNL_visual_soap.clear()
1864
1866 fname, discard_unmodified = select_visual_progress_note_template(parent = self)
1867 if fname is None:
1868 return False
1869
1870 if self.problem is None:
1871 issue = None
1872 episode = None
1873 elif self.problem['type'] == 'issue':
1874 issue = self.problem['pk_health_issue']
1875 episode = None
1876 else:
1877 issue = self.problem['pk_health_issue']
1878 episode = gmEMRStructItems.problem2episode(self.problem)
1879
1880 wx.CallAfter (
1881 edit_visual_progress_note,
1882 filename = fname,
1883 episode = episode,
1884 discard_unmodified = discard_unmodified,
1885 health_issue = issue
1886 )
1887
1888 - def save(self, emr=None, episode_name_candidates=None, encounter=None):
1889
1890 if self.empty:
1891 return True
1892
1893
1894 if (self.problem is None) or (self.problem['type'] == 'issue'):
1895 episode = self.__create_new_episode(emr = emr, episode_name_candidates = episode_name_candidates)
1896
1897 if episode is None:
1898 return False
1899
1900 else:
1901 episode = emr.problem2episode(self.problem)
1902
1903 if encounter is None:
1904 encounter = emr.current_encounter['pk_encounter']
1905
1906 soap_notes = []
1907 for note in self.soap:
1908 saved, data = gmClinNarrative.create_clin_narrative (
1909 soap_cat = note[0],
1910 narrative = note[1],
1911 episode_id = episode['pk_episode'],
1912 encounter_id = encounter
1913 )
1914 if saved:
1915 soap_notes.append(data)
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928 if self.problem is not None:
1929 if self.problem['type'] == 'episode':
1930 episode['summary'] = self._TCTRL_episode_summary.GetValue().strip()
1931 episode.save()
1932
1933
1934 episode.generic_codes = [ d['data'] for d in self._PRW_episode_codes.GetData() ]
1935
1936 return True
1937
1938
1939
1941
1942 episode_name_candidates.append(self._TCTRL_episode_summary.GetValue().strip())
1943 for candidate in episode_name_candidates:
1944 if candidate is None:
1945 continue
1946 epi_name = candidate.strip().replace('\r', '//').replace('\n', '//')
1947 break
1948
1949 dlg = wx.TextEntryDialog (
1950 parent = self,
1951 message = _('Enter a short working name for this new problem:'),
1952 caption = _('Creating a problem (episode) to save the notelet under ...'),
1953 defaultValue = epi_name,
1954 style = wx.OK | wx.CANCEL | wx.CENTRE
1955 )
1956 decision = dlg.ShowModal()
1957 if decision != wx.ID_OK:
1958 return None
1959
1960 epi_name = dlg.GetValue().strip()
1961 if epi_name == u'':
1962 gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note'))
1963 return None
1964
1965
1966 new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True)
1967 new_episode['summary'] = self._TCTRL_episode_summary.GetValue().strip()
1968 new_episode.save()
1969
1970 if self.problem is not None:
1971 issue = emr.problem2issue(self.problem)
1972 if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True):
1973 gmGuiHelpers.gm_show_warning (
1974 _(
1975 'The new episode:\n'
1976 '\n'
1977 ' "%s"\n'
1978 '\n'
1979 'will remain unassociated despite the editor\n'
1980 'having been invoked from the health issue:\n'
1981 '\n'
1982 ' "%s"'
1983 ) % (
1984 new_episode['description'],
1985 issue['description']
1986 ),
1987 _('saving progress note')
1988 )
1989
1990 return new_episode
1991
1992
1993
1995 for field in self.soap_fields:
1996 wx_expando.EVT_ETC_LAYOUT_NEEDED(field, field.GetId(), self._on_expando_needs_layout)
1997 wx_expando.EVT_ETC_LAYOUT_NEEDED(self._TCTRL_episode_summary, self._TCTRL_episode_summary.GetId(), self._on_expando_needs_layout)
1998 gmDispatcher.connect(signal = u'blobs.doc_obj_mod_db', receiver = self._refresh_visual_soap)
1999
2002
2004
2005
2006
2007
2008 self.FitInside()
2009
2010 if self.HasScrollbar(wx.VERTICAL):
2011
2012 expando = self.FindWindowById(evt.GetId())
2013 y_expando = expando.GetPositionTuple()[1]
2014 h_expando = expando.GetSizeTuple()[1]
2015 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1
2016 if expando.NumberOfLines == 0:
2017 no_of_lines = 1
2018 else:
2019 no_of_lines = expando.NumberOfLines
2020 y_cursor = int(round((float(line_cursor) / no_of_lines) * h_expando))
2021 y_desired_visible = y_expando + y_cursor
2022
2023 y_view = self.ViewStart[1]
2024 h_view = self.GetClientSizeTuple()[1]
2025
2026
2027
2028
2029
2030
2031
2032
2033 if y_desired_visible < y_view:
2034
2035 self.Scroll(0, y_desired_visible)
2036
2037 if y_desired_visible > h_view:
2038
2039 self.Scroll(0, y_desired_visible)
2040
2041
2042
2044 soap_notes = []
2045
2046 tmp = self._TCTRL_Soap.GetValue().strip()
2047 if tmp != u'':
2048 soap_notes.append(['s', tmp])
2049
2050 tmp = self._TCTRL_sOap.GetValue().strip()
2051 if tmp != u'':
2052 soap_notes.append(['o', tmp])
2053
2054 tmp = self._TCTRL_soAp.GetValue().strip()
2055 if tmp != u'':
2056 soap_notes.append(['a', tmp])
2057
2058 tmp = self._TCTRL_soaP.GetValue().strip()
2059 if tmp != u'':
2060 soap_notes.append(['p', tmp])
2061
2062 return soap_notes
2063
2064 soap = property(_get_soap, lambda x:x)
2065
2067
2068
2069 for field in self.soap_fields:
2070 if field.GetValue().strip() != u'':
2071 return False
2072
2073
2074 summary = self._TCTRL_episode_summary.GetValue().strip()
2075 if self.problem is None:
2076 if summary != u'':
2077 return False
2078 elif self.problem['type'] == u'issue':
2079 if summary != u'':
2080 return False
2081 else:
2082 if self.problem['summary'] is None:
2083 if summary != u'':
2084 return False
2085 else:
2086 if summary != self.problem['summary'].strip():
2087 return False
2088
2089
2090 new_codes = self._PRW_episode_codes.GetData()
2091 if self.problem is None:
2092 if len(new_codes) > 0:
2093 return False
2094 elif self.problem['type'] == u'issue':
2095 if len(new_codes) > 0:
2096 return False
2097 else:
2098 old_code_pks = self.problem.generic_codes
2099 if len(old_code_pks) != len(new_codes):
2100 return False
2101 for code in new_codes:
2102 if code['data'] not in old_code_pks:
2103 return False
2104
2105 return True
2106
2107 empty = property(_get_empty, lambda x:x)
2108
2109 -class cSoapLineTextCtrl(wx_expando.ExpandoTextCtrl, gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin):
2110
2111 - def __init__(self, *args, **kwargs):
2112
2113 wx_expando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
2114 gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin.__init__(self)
2115 self.enable_keyword_expansions()
2116
2117 self.__register_interests()
2118
2119
2120
2121 - def _wrapLine(self, line, dc, width):
2122
2123 if (wx.MAJOR_VERSION >= 2) and (wx.MINOR_VERSION > 8):
2124 return wx_expando.ExpandoTextCtrl._wrapLine(line, dc, width)
2125
2126
2127
2128
2129 pte = dc.GetPartialTextExtents(line)
2130 width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X)
2131 idx = 0
2132 start = 0
2133 count = 0
2134 spc = -1
2135 while idx < len(pte):
2136 if line[idx] == ' ':
2137 spc = idx
2138 if pte[idx] - start > width:
2139
2140 count += 1
2141
2142 if spc != -1:
2143 idx = spc + 1
2144 spc = -1
2145 if idx < len(pte):
2146 start = pte[idx]
2147 else:
2148 idx += 1
2149 return count
2150
2151
2152
2154
2155
2156 wx.EVT_SET_FOCUS(self, self.__on_focus)
2157
2158 - def __on_focus(self, evt):
2159 evt.Skip()
2160 wx.CallAfter(self._after_on_focus)
2161
2162 - def _after_on_focus(self):
2163
2164
2165
2166
2167 if not self:
2168 return
2169
2170 evt = wx.PyCommandEvent(wx_expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
2171 evt.SetEventObject(self)
2172
2173
2174
2175
2176 self.GetEventHandler().ProcessEvent(evt)
2177
2178
2179
2180
2211
2212 cmd = gmCfgWidgets.configure_string_option (
2213 message = _(
2214 'Enter the shell command with which to start\n'
2215 'the image editor for visual progress notes.\n'
2216 '\n'
2217 'Any "%(img)s" included with the arguments\n'
2218 'will be replaced by the file name of the\n'
2219 'note template.'
2220 ),
2221 option = u'external.tools.visual_soap_editor_cmd',
2222 bias = 'user',
2223 default_value = None,
2224 validator = is_valid
2225 )
2226
2227 return cmd
2228
2230 if parent is None:
2231 parent = wx.GetApp().GetTopWindow()
2232
2233 dlg = wx.FileDialog (
2234 parent = parent,
2235 message = _('Choose file to use as template for new visual progress note'),
2236 defaultDir = os.path.expanduser('~'),
2237 defaultFile = '',
2238
2239 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST
2240 )
2241 result = dlg.ShowModal()
2242
2243 if result == wx.ID_CANCEL:
2244 dlg.Destroy()
2245 return None
2246
2247 full_filename = dlg.GetPath()
2248 dlg.Hide()
2249 dlg.Destroy()
2250 return full_filename
2251
2253
2254 if parent is None:
2255 parent = wx.GetApp().GetTopWindow()
2256
2257 dlg = gmGuiHelpers.c3ButtonQuestionDlg (
2258 parent,
2259 -1,
2260 caption = _('Visual progress note source'),
2261 question = _('From which source do you want to pick the image template ?'),
2262 button_defs = [
2263 {'label': _('Database'), 'tooltip': _('List of templates in the database.'), 'default': True},
2264 {'label': _('File'), 'tooltip': _('Files in the filesystem.'), 'default': False},
2265 {'label': _('Device'), 'tooltip': _('Image capture devices (scanners, cameras, etc)'), 'default': False}
2266 ]
2267 )
2268 result = dlg.ShowModal()
2269 dlg.Destroy()
2270
2271
2272 if result == wx.ID_YES:
2273 _log.debug('visual progress note template from: database template')
2274 from Gnumed.wxpython import gmFormWidgets
2275 template = gmFormWidgets.manage_form_templates (
2276 parent = parent,
2277 template_types = [gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE],
2278 active_only = True
2279 )
2280 if template is None:
2281 return (None, None)
2282 filename = template.export_to_file()
2283 if filename is None:
2284 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note template for [%s].') % template['name_long'])
2285 return (None, None)
2286 return (filename, True)
2287
2288
2289 if result == wx.ID_NO:
2290 _log.debug('visual progress note template from: disk file')
2291 fname = select_file_as_visual_progress_note_template(parent = parent)
2292 if fname is None:
2293 return (None, None)
2294
2295 ext = os.path.splitext(fname)[1]
2296 tmp_name = gmTools.get_unique_filename(suffix = ext)
2297 _log.debug('visual progress note from file: [%s] -> [%s]', fname, tmp_name)
2298 shutil.copy2(fname, tmp_name)
2299 return (tmp_name, False)
2300
2301
2302 if result == wx.ID_CANCEL:
2303 _log.debug('visual progress note template from: image capture device')
2304 fnames = gmDocumentWidgets.acquire_images_from_capture_device(device = None, calling_window = parent)
2305 if fnames is None:
2306 return (None, None)
2307 if len(fnames) == 0:
2308 return (None, None)
2309 return (fnames[0], False)
2310
2311 _log.debug('no visual progress note template source selected')
2312 return (None, None)
2313
2315 """This assumes <filename> contains an image which can be handled by the configured image editor."""
2316
2317 if doc_part is not None:
2318 filename = doc_part.export_to_file()
2319 if filename is None:
2320 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.'))
2321 return None
2322
2323 dbcfg = gmCfg.cCfgSQL()
2324 cmd = dbcfg.get2 (
2325 option = u'external.tools.visual_soap_editor_cmd',
2326 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
2327 bias = 'user'
2328 )
2329
2330 if cmd is None:
2331 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = False)
2332 cmd = configure_visual_progress_note_editor()
2333 if cmd is None:
2334 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = True)
2335 return None
2336
2337 if u'%(img)s' in cmd:
2338 cmd = cmd % {u'img': filename}
2339 else:
2340 cmd = u'%s %s' % (cmd, filename)
2341
2342 if discard_unmodified:
2343 original_stat = os.stat(filename)
2344 original_md5 = gmTools.file2md5(filename)
2345
2346 success = gmShellAPI.run_command_in_shell(cmd, blocking = True)
2347 if not success:
2348 gmGuiHelpers.gm_show_error (
2349 _(
2350 'There was a problem with running the editor\n'
2351 'for visual progress notes.\n'
2352 '\n'
2353 ' [%s]\n'
2354 '\n'
2355 ) % cmd,
2356 _('Editing visual progress note')
2357 )
2358 return None
2359
2360 try:
2361 open(filename, 'r').close()
2362 except StandardError:
2363 _log.exception('problem accessing visual progress note file [%s]', filename)
2364 gmGuiHelpers.gm_show_error (
2365 _(
2366 'There was a problem reading the visual\n'
2367 'progress note from the file:\n'
2368 '\n'
2369 ' [%s]\n'
2370 '\n'
2371 ) % filename,
2372 _('Saving visual progress note')
2373 )
2374 return None
2375
2376 if discard_unmodified:
2377 modified_stat = os.stat(filename)
2378
2379 if original_stat.st_size == modified_stat.st_size:
2380 modified_md5 = gmTools.file2md5(filename)
2381
2382 if original_md5 == modified_md5:
2383 _log.debug('visual progress note (template) not modified')
2384
2385 msg = _(
2386 u'You either created a visual progress note from a template\n'
2387 u'in the database (rather than from a file on disk) or you\n'
2388 u'edited an existing visual progress note.\n'
2389 u'\n'
2390 u'The template/original was not modified at all, however.\n'
2391 u'\n'
2392 u'Do you still want to save the unmodified image as a\n'
2393 u'visual progress note into the EMR of the patient ?\n'
2394 )
2395 save_unmodified = gmGuiHelpers.gm_show_question (
2396 msg,
2397 _('Saving visual progress note')
2398 )
2399 if not save_unmodified:
2400 _log.debug('user discarded unmodified note')
2401 return
2402
2403 if doc_part is not None:
2404 _log.debug('updating visual progress note')
2405 doc_part.update_data_from_file(fname = filename)
2406 doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2407 return None
2408
2409 if not isinstance(episode, gmEMRStructItems.cEpisode):
2410 if episode is None:
2411 episode = _('visual progress notes')
2412 pat = gmPerson.gmCurrentPatient()
2413 emr = pat.get_emr()
2414 episode = emr.add_episode(episode_name = episode.strip(), pk_health_issue = health_issue, is_open = False)
2415
2416 doc = gmDocumentWidgets.save_file_as_new_document (
2417 filename = filename,
2418 document_type = gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE,
2419 episode = episode,
2420 unlock_patient = False
2421 )
2422 doc.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2423
2424 return doc
2425
2427 """Phrasewheel to allow selection of visual SOAP template."""
2428
2430
2431 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
2432
2433 query = u"""
2434 SELECT
2435 pk AS data,
2436 name_short AS list_label,
2437 name_sort AS field_label
2438 FROM
2439 ref.paperwork_templates
2440 WHERE
2441 fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND (
2442 name_long %%(fragment_condition)s
2443 OR
2444 name_short %%(fragment_condition)s
2445 )
2446 ORDER BY list_label
2447 LIMIT 15
2448 """ % gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE
2449
2450 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
2451 mp.setThresholds(2, 3, 5)
2452
2453 self.matcher = mp
2454 self.selection_only = True
2455
2461
2462 from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl
2463
2465
2470
2471
2472
2473 - def refresh(self, document_folder=None, episodes=None, encounter=None):
2474
2475 self.clear()
2476 if document_folder is not None:
2477 soap_docs = document_folder.get_visual_progress_notes(episodes = episodes, encounter = encounter)
2478 if len(soap_docs) > 0:
2479 for soap_doc in soap_docs:
2480 parts = soap_doc.parts
2481 if len(parts) == 0:
2482 continue
2483 part = parts[0]
2484 fname = part.export_to_file()
2485 if fname is None:
2486 continue
2487
2488
2489 img = gmGuiHelpers.file2scaled_image (
2490 filename = fname,
2491 height = 30
2492 )
2493
2494 bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER)
2495
2496
2497 img = gmGuiHelpers.file2scaled_image (
2498 filename = fname,
2499 height = 150
2500 )
2501 tip = agw_stt.SuperToolTip (
2502 u'',
2503 bodyImage = img,
2504 header = _('Created: %s') % gmDateTime.pydt_strftime(part['date_generated'], '%Y %b %d'),
2505 footer = gmTools.coalesce(part['doc_comment'], u'').strip()
2506 )
2507 tip.SetTopGradientColor('white')
2508 tip.SetMiddleGradientColor('white')
2509 tip.SetBottomGradientColor('white')
2510 tip.SetTarget(bmp)
2511
2512 bmp.doc_part = part
2513 bmp.Bind(wx.EVT_LEFT_UP, self._on_bitmap_leftclicked)
2514
2515 self._SZR_soap.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM | wx.EXPAND, 3)
2516 self.__bitmaps.append(bmp)
2517
2518 self.GetParent().Layout()
2519
2521 while len(self._SZR_soap.GetChildren()) > 0:
2522 self._SZR_soap.Detach(0)
2523
2524
2525 for bmp in self.__bitmaps:
2526 bmp.Destroy()
2527 self.__bitmaps = []
2528
2530 wx.CallAfter (
2531 edit_visual_progress_note,
2532 doc_part = evt.GetEventObject().doc_part,
2533 discard_unmodified = True
2534 )
2535
2536
2537
2538 from Gnumed.wxGladeWidgets import wxgSimpleSoapPluginPnl
2539
2540 -class cSimpleSoapPluginPnl(wxgSimpleSoapPluginPnl.wxgSimpleSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
2550
2551
2552
2554 self._LCTRL_problems.set_columns(columns = [_('Problem list')])
2555 self._LCTRL_problems.activate_callback = self._on_problem_activated
2556 self._LCTRL_problems.item_tooltip_callback = self._on_get_problem_tooltip
2557
2558 self._splitter_main.SetSashGravity(0.5)
2559 splitter_width = self._splitter_main.GetSizeTuple()[0]
2560 self._splitter_main.SetSashPosition(splitter_width / 2, True)
2561
2562 self._TCTRL_soap.Disable()
2563 self._BTN_save_soap.Disable()
2564 self._BTN_clear_soap.Disable()
2565
2567 self._LCTRL_problems.set_string_items()
2568 self._TCTRL_soap_problem.SetValue(_('<above, double-click problem to start entering SOAP note>'))
2569 self._TCTRL_soap.SetValue(u'')
2570 self._CHBOX_filter_by_problem.SetLabel(_('&Filter by problem'))
2571 self._TCTRL_journal.SetValue(u'')
2572
2573 self._TCTRL_soap.Disable()
2574 self._BTN_save_soap.Disable()
2575 self._BTN_clear_soap.Disable()
2576
2578 if not self.__curr_pat.connected:
2579 return None
2580
2581 if self.__problem is None:
2582 return None
2583
2584 saved = self.__curr_pat.emr.add_clin_narrative (
2585 note = self._TCTRL_soap.GetValue().strip(),
2586 soap_cat = u'u',
2587 episode = self.__problem
2588 )
2589
2590 if saved is None:
2591 return False
2592
2593 self._TCTRL_soap.SetValue(u'')
2594 self.__refresh_journal()
2595 return True
2596
2598 if self._TCTRL_soap.GetValue().strip() == u'':
2599 return True
2600 if self.__problem is None:
2601
2602 self._TCTRL_soap.SetValue(u'')
2603 return None
2604 save_it = gmGuiHelpers.gm_show_question (
2605 title = _('Saving SOAP note'),
2606 question = _('Do you want to save the SOAP note ?')
2607 )
2608 if save_it:
2609 return self.__save_soap()
2610 return False
2611
2622
2643
2644
2645
2647 """Configure enabled event signals."""
2648
2649 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
2650 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
2651 gmDispatcher.connect(signal = u'clin.episode_mod_db', receiver = self._on_episode_issue_mod_db)
2652 gmDispatcher.connect(signal = u'clin.health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
2653
2654
2655 self.__curr_pat.register_pre_selection_callback(callback = self._pre_selection_callback)
2656 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
2657
2659 """Another patient is about to be activated.
2660
2661 Patient change will not proceed before this returns True.
2662 """
2663 if not self.__curr_pat.connected:
2664 return True
2665 self.__perhaps_save_soap()
2666 self.__problem = None
2667 return True
2668
2670 """The client is about to be shut down.
2671
2672 Shutdown will not proceed before this returns.
2673 """
2674 if not self.__curr_pat.connected:
2675 return
2676 if not self.__save_soap():
2677 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save SimpleNotes SOAP note.'), beep = True)
2678 return
2679
2681 wx.CallAfter(self.__reset_ui)
2682
2684 wx.CallAfter(self._schedule_data_reget)
2685
2687 wx.CallAfter(self._schedule_data_reget)
2688
2690 self.__perhaps_save_soap()
2691 epi = self._LCTRL_problems.get_selected_item_data(only_one = True)
2692 self._TCTRL_soap_problem.SetValue(_('Progress note: %s%s') % (
2693 epi['description'],
2694 gmTools.coalesce(epi['health_issue'], u'', u' (%s)')
2695 ))
2696 self.__problem = epi
2697 self._TCTRL_soap.SetValue(u'')
2698
2699 self._TCTRL_soap.Enable()
2700 self._BTN_save_soap.Enable()
2701 self._BTN_clear_soap.Enable()
2702
2717
2719 event.Skip()
2720 self.__refresh_journal()
2721
2723 event.Skip()
2724 self.__refresh_journal()
2725
2740
2747
2755
2759
2763
2764
2765
2767 self.__refresh_problem_list()
2768 self.__refresh_journal()
2769 self._TCTRL_soap.SetValue(u'')
2770 return True
2771
2772
2773
2774
2775 if __name__ == '__main__':
2776
2777 if len(sys.argv) < 2:
2778 sys.exit()
2779
2780 if sys.argv[1] != 'test':
2781 sys.exit()
2782
2783 gmI18N.activate_locale()
2784 gmI18N.install_domain(domain = 'gnumed')
2785
2786
2795
2804
2811
2824
2825
2826 test_select_narrative()
2827
2828
2829
2830
2831