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 gmSurgery
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 gmRegetMixin
51 from Gnumed.wxpython import gmPhraseWheel
52 from Gnumed.wxpython import gmGuiHelpers
53 from Gnumed.wxpython import gmPatSearchWidgets
54 from Gnumed.wxpython import gmCfgWidgets
55 from Gnumed.wxpython import gmDocumentWidgets
56 from Gnumed.wxpython import gmKeywordExpansionWidgets
57
58 from Gnumed.exporters import gmPatientExporter
59
60
61 _log = logging.getLogger('gm.ui')
62
63
64
66
67
68 if patient is None:
69 patient = gmPerson.gmCurrentPatient()
70
71 if not patient.connected:
72 gmDispatcher.send(signal = 'statustext', msg = _('Cannot move progress notes. No active patient.'))
73 return False
74
75 if parent is None:
76 parent = wx.GetApp().GetTopWindow()
77
78 emr = patient.get_emr()
79
80 if encounters is None:
81 encs = emr.get_encounters(episodes = episodes)
82 encounters = gmEMRStructWidgets.select_encounters (
83 parent = parent,
84 patient = patient,
85 single_selection = False,
86 encounters = encs
87 )
88
89 if encounters is None:
90 return True
91
92 if len(encounters) == 0:
93 return True
94
95 notes = emr.get_clin_narrative (
96 encounters = encounters,
97 episodes = episodes
98 )
99
100
101 if move_all:
102 selected_narr = notes
103 else:
104 selected_narr = gmListWidgets.get_choices_from_list (
105 parent = parent,
106 caption = _('Moving progress notes between encounters ...'),
107 single_selection = False,
108 can_return_empty = True,
109 data = notes,
110 msg = _('\n Select the progress notes to move from the list !\n\n'),
111 columns = [_('when'), _('who'), _('type'), _('entry')],
112 choices = [
113 [ narr['date'].strftime('%x %H:%M'),
114 narr['provider'],
115 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
116 narr['narrative'].replace('\n', '/').replace('\r', '/')
117 ] for narr in notes
118 ]
119 )
120
121 if not selected_narr:
122 return True
123
124
125 enc2move2 = gmEMRStructWidgets.select_encounters (
126 parent = parent,
127 patient = patient,
128 single_selection = True
129 )
130
131 if not enc2move2:
132 return True
133
134 for narr in selected_narr:
135 narr['pk_encounter'] = enc2move2['pk_encounter']
136 narr.save()
137
138 return True
139
141
142
143 if patient is None:
144 patient = gmPerson.gmCurrentPatient()
145
146 if not patient.connected:
147 gmDispatcher.send(signal = 'statustext', msg = _('Cannot edit progress notes. No active patient.'))
148 return False
149
150 if parent is None:
151 parent = wx.GetApp().GetTopWindow()
152
153 emr = patient.get_emr()
154
155 def delete(item):
156 if item is None:
157 return False
158 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
159 parent,
160 -1,
161 caption = _('Deleting progress note'),
162 question = _(
163 'Are you positively sure you want to delete this\n'
164 'progress note from the medical record ?\n'
165 '\n'
166 'Note that even if you chose to delete the entry it will\n'
167 'still be (invisibly) kept in the audit trail to protect\n'
168 'you from litigation because physical deletion is known\n'
169 'to be unlawful in some jurisdictions.\n'
170 ),
171 button_defs = (
172 {'label': _('Delete'), 'tooltip': _('Yes, delete the progress note.'), 'default': False},
173 {'label': _('Cancel'), 'tooltip': _('No, do NOT delete the progress note.'), 'default': True}
174 )
175 )
176 decision = dlg.ShowModal()
177
178 if decision != wx.ID_YES:
179 return False
180
181 gmClinNarrative.delete_clin_narrative(narrative = item['pk_narrative'])
182 return True
183
184 def edit(item):
185 if item is None:
186 return False
187
188 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
189 parent,
190 -1,
191 title = _('Editing progress note'),
192 msg = _('This is the original progress note:'),
193 data = item.format(left_margin = u' ', fancy = True),
194 text = item['narrative']
195 )
196 decision = dlg.ShowModal()
197
198 if decision != wx.ID_SAVE:
199 return False
200
201 val = dlg.value
202 dlg.Destroy()
203 if val.strip() == u'':
204 return False
205
206 item['narrative'] = val
207 item.save_payload()
208
209 return True
210
211 def refresh(lctrl):
212 notes = emr.get_clin_narrative (
213 encounters = encounters,
214 episodes = episodes,
215 providers = [ gmStaff.gmCurrentProvider()['short_alias'] ]
216 )
217 lctrl.set_string_items(items = [
218 [ narr['date'].strftime('%x %H:%M'),
219 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
220 narr['narrative'].replace('\n', '/').replace('\r', '/')
221 ] for narr in notes
222 ])
223 lctrl.set_data(data = notes)
224
225
226 gmListWidgets.get_choices_from_list (
227 parent = parent,
228 caption = _('Managing progress notes'),
229 msg = _(
230 '\n'
231 ' This list shows the progress notes by %s.\n'
232 '\n'
233 ) % gmStaff.gmCurrentProvider()['short_alias'],
234 columns = [_('when'), _('type'), _('entry')],
235 single_selection = True,
236 can_return_empty = False,
237 edit_callback = edit,
238 delete_callback = delete,
239 refresh_callback = refresh
240 )
241
243
244 if parent is None:
245 parent = wx.GetApp().GetTopWindow()
246
247 searcher = wx.TextEntryDialog (
248 parent = parent,
249 message = _('Enter (regex) term to search for across all EMRs:'),
250 caption = _('Text search across all EMRs'),
251 style = wx.OK | wx.CANCEL | wx.CENTRE
252 )
253 result = searcher.ShowModal()
254
255 if result != wx.ID_OK:
256 return
257
258 wx.BeginBusyCursor()
259 term = searcher.GetValue()
260 searcher.Destroy()
261 results = gmClinNarrative.search_text_across_emrs(search_term = term)
262 wx.EndBusyCursor()
263
264 if len(results) == 0:
265 gmGuiHelpers.gm_show_info (
266 _(
267 'Nothing found for search term:\n'
268 ' "%s"'
269 ) % term,
270 _('Search results')
271 )
272 return
273
274 items = [ [gmPerson.cIdentity(aPK_obj =
275 r['pk_patient'])['description_gender'], r['narrative'],
276 r['src_table']] for r in results ]
277
278 selected_patient = gmListWidgets.get_choices_from_list (
279 parent = parent,
280 caption = _('Search results for %s') % term,
281 choices = items,
282 columns = [_('Patient'), _('Match'), _('Match location')],
283 data = [ r['pk_patient'] for r in results ],
284 single_selection = True,
285 can_return_empty = False
286 )
287
288 if selected_patient is None:
289 return
290
291 wx.CallAfter(gmPatSearchWidgets.set_active_patient, patient = gmPerson.cIdentity(aPK_obj = selected_patient))
292
294
295
296 if patient is None:
297 patient = gmPerson.gmCurrentPatient()
298
299 if not patient.connected:
300 gmDispatcher.send(signal = 'statustext', msg = _('Cannot search EMR. No active patient.'))
301 return False
302
303 if parent is None:
304 parent = wx.GetApp().GetTopWindow()
305
306 searcher = wx.TextEntryDialog (
307 parent = parent,
308 message = _('Enter search term:'),
309 caption = _('Text search of entire EMR of active patient'),
310 style = wx.OK | wx.CANCEL | wx.CENTRE
311 )
312 result = searcher.ShowModal()
313
314 if result != wx.ID_OK:
315 searcher.Destroy()
316 return False
317
318 wx.BeginBusyCursor()
319 val = searcher.GetValue()
320 searcher.Destroy()
321 emr = patient.get_emr()
322 rows = emr.search_narrative_simple(val)
323 wx.EndBusyCursor()
324
325 if len(rows) == 0:
326 gmGuiHelpers.gm_show_info (
327 _(
328 'Nothing found for search term:\n'
329 ' "%s"'
330 ) % val,
331 _('Search results')
332 )
333 return True
334
335 txt = u''
336 for row in rows:
337 txt += u'%s: %s\n' % (
338 row['soap_cat'],
339 row['narrative']
340 )
341
342 txt += u' %s: %s - %s %s\n' % (
343 _('Encounter'),
344 row['encounter_started'].strftime('%x %H:%M'),
345 row['encounter_ended'].strftime('%H:%M'),
346 row['encounter_type']
347 )
348 txt += u' %s: %s\n' % (
349 _('Episode'),
350 row['episode']
351 )
352 txt += u' %s: %s\n\n' % (
353 _('Health issue'),
354 row['health_issue']
355 )
356
357 msg = _(
358 'Search term was: "%s"\n'
359 '\n'
360 'Search results:\n\n'
361 '%s\n'
362 ) % (val, txt)
363
364 dlg = wx.MessageDialog (
365 parent = parent,
366 message = msg,
367 caption = _('Search results for %s') % val,
368 style = wx.OK | wx.STAY_ON_TOP
369 )
370 dlg.ShowModal()
371 dlg.Destroy()
372
373 return True
374
376
377
378 pat = gmPerson.gmCurrentPatient()
379 if not pat.connected:
380 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR for Medistar. No active patient.'))
381 return False
382
383 if encounter is None:
384 encounter = pat.get_emr().active_encounter
385
386 if parent is None:
387 parent = wx.GetApp().GetTopWindow()
388
389
390 aWildcard = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files"))
391
392 aDefDir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed','export')))
393
394 fname = '%s-%s-%s-%s-%s.txt' % (
395 'Medistar-MD',
396 time.strftime('%Y-%m-%d',time.localtime()),
397 pat['lastnames'].replace(' ', '-'),
398 pat['firstnames'].replace(' ', '_'),
399 pat.get_formatted_dob(format = '%Y-%m-%d')
400 )
401 dlg = wx.FileDialog (
402 parent = parent,
403 message = _("Save EMR extract for MEDISTAR import as..."),
404 defaultDir = aDefDir,
405 defaultFile = fname,
406 wildcard = aWildcard,
407 style = wx.SAVE
408 )
409 choice = dlg.ShowModal()
410 fname = dlg.GetPath()
411 dlg.Destroy()
412 if choice != wx.ID_OK:
413 return False
414
415 wx.BeginBusyCursor()
416 _log.debug('exporting encounter for medistar import to [%s]', fname)
417 exporter = gmPatientExporter.cMedistarSOAPExporter()
418 successful, fname = exporter.export_to_file (
419 filename = fname,
420 encounter = encounter,
421 soap_cats = u'soapu',
422 export_to_import_file = True
423 )
424 if not successful:
425 gmGuiHelpers.gm_show_error (
426 _('Error exporting progress notes for MEDISTAR import.'),
427 _('MEDISTAR progress notes export')
428 )
429 wx.EndBusyCursor()
430 return False
431
432 gmDispatcher.send(signal = 'statustext', msg = _('Successfully exported progress notes into file [%s] for Medistar import.') % fname, beep=False)
433
434 wx.EndBusyCursor()
435 return True
436
438
439 pat = gmPerson.gmCurrentPatient()
440 emr = pat.get_emr()
441
442
443
444
445
446
447
448 if parent is None:
449 parent = wx.GetApp().GetTopWindow()
450
451 if soap_cats is None:
452 soap_cats = u'soapu'
453 soap_cats = list(soap_cats)
454 i18n_soap_cats = [ gmClinNarrative.soap_cat2l10n[cat].upper() for cat in soap_cats ]
455
456 selected_soap = {}
457
458
459
460 def get_soap_tooltip(soap):
461 return soap.format(fancy = True, width = 60)
462
463 def pick_soap_from_issue(issue):
464
465 if issue is None:
466 return False
467
468 narr_for_issue = emr.get_clin_narrative(issues = [issue['pk_health_issue']], soap_cats = soap_cats)
469
470 if len(narr_for_issue) == 0:
471 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for this health issue.'))
472 return True
473
474 selected_narr = gmListWidgets.get_choices_from_list (
475 parent = parent,
476 msg = _('Pick the [%s] narrative you want to include in the report.') % u'/'.join(i18n_soap_cats),
477 caption = _('Picking [%s] from %s%s%s') % (
478 u'/'.join(i18n_soap_cats),
479 gmTools.u_left_double_angle_quote,
480 issue['description'],
481 gmTools.u_right_double_angle_quote
482 ),
483 columns = [_('When'), _('Who'), _('Type'), _('Entry')],
484 choices = [ [
485 gmDateTime.pydt_strftime(narr['date'], '%Y %b %d %H:%M', accuracy = gmDateTime.acc_minutes),
486 narr['provider'],
487 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
488 narr['narrative'].replace('\n', '//').replace('\r', '//')
489 ] for narr in narr_for_issue ],
490 data = narr_for_issue,
491
492
493 single_selection = False,
494 can_return_empty = False,
495 list_tooltip_callback = get_soap_tooltip
496 )
497
498 if selected_narr is None:
499 return True
500
501 for narr in selected_narr:
502 selected_soap[narr['pk_narrative']] = narr
503
504 return True
505
506 def edit_issue(issue):
507 return gmEMRStructWidgets.edit_health_issue(parent = parent, issue = issue)
508
509 def refresh_issues(lctrl):
510
511 issues = emr.health_issues
512 lctrl.set_string_items ([ [
513 gmTools.bool2subst(i['is_confidential'], _('!! CONFIDENTIAL !!'), u''),
514 i['description'],
515 gmTools.bool2subst(i['is_active'], _('active'), _('inactive'))
516 ] for i in issues
517 ])
518 lctrl.set_data(issues)
519
520 def get_issue_tooltip(issue):
521 return issue.format (
522 patient = pat,
523 with_encounters = False,
524 with_medications = False,
525 with_hospital_stays = False,
526 with_procedures = False,
527 with_family_history = False,
528 with_documents = False,
529 with_tests = False,
530 with_vaccinations = False
531 )
532
533
534
535 issues_picked_from = gmListWidgets.get_choices_from_list (
536 parent = parent,
537 msg = _('\n Select the issue you want to report on.'),
538 caption = _('Picking [%s] from health issues') % u'/'.join(i18n_soap_cats),
539 columns = [_('Privacy'), _('Issue'), _('Status')],
540 edit_callback = edit_issue,
541 refresh_callback = refresh_issues,
542 single_selection = True,
543 can_return_empty = True,
544 ignore_OK_button = False,
545 left_extra_button = (
546 _('&Pick notes'),
547 _('Pick [%s] entries from selected health issue') % u'/'.join(i18n_soap_cats),
548 pick_soap_from_issue
549 ),
550 list_tooltip_callback = get_issue_tooltip
551 )
552
553 if issues_picked_from is None:
554 return []
555
556 return selected_soap.values()
557
558
559
560
561
562
563
564
566
567 pat = gmPerson.gmCurrentPatient()
568 emr = pat.get_emr()
569
570 all_epis = [ epi for epi in emr.get_episodes(order_by = u'description') if epi.has_narrative ]
571 if len(all_epis) == 0:
572 gmDispatcher.send(signal = 'statustext', msg = _('No episodes with progress notes found.'))
573 return []
574
575 if parent is None:
576 parent = wx.GetApp().GetTopWindow()
577
578 if soap_cats is None:
579 soap_cats = u'soapu'
580 soap_cats = list(soap_cats)
581 i18n_soap_cats = [ gmClinNarrative.soap_cat2l10n[cat].upper() for cat in soap_cats ]
582
583 selected_soap = {}
584
585
586
587 def get_soap_tooltip(soap):
588 return soap.format(fancy = True, width = 60)
589
590 def pick_soap_from_episode(episode):
591
592 if episode is None:
593 return False
594
595 narr_for_epi = emr.get_clin_narrative(episodes = [episode['pk_episode']], soap_cats = soap_cats)
596
597 if len(narr_for_epi) == 0:
598 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episode.'))
599 return True
600
601 selected_narr = gmListWidgets.get_choices_from_list (
602 parent = parent,
603 msg = _('Pick the [%s] narrative you want to include in the report.') % u'/'.join(i18n_soap_cats),
604 caption = _('Picking [%s] from %s%s%s') % (
605 u'/'.join(i18n_soap_cats),
606 gmTools.u_left_double_angle_quote,
607 episode['description'],
608 gmTools.u_right_double_angle_quote
609 ),
610 columns = [_('When'), _('Who'), _('Type'), _('Entry')],
611 choices = [ [
612 gmDateTime.pydt_strftime(narr['date'], '%Y %b %d %H:%M', accuracy = gmDateTime.acc_minutes),
613 narr['provider'],
614 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
615 narr['narrative'].replace('\n', '//').replace('\r', '//')
616 ] for narr in narr_for_epi ],
617 data = narr_for_epi,
618
619
620 single_selection = False,
621 can_return_empty = False,
622 list_tooltip_callback = get_soap_tooltip
623 )
624
625 if selected_narr is None:
626 return True
627
628 for narr in selected_narr:
629 selected_soap[narr['pk_narrative']] = narr
630
631 return True
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648 def edit_episode(episode):
649 return gmEMRStructWidgets.edit_episode(parent = parent, episode = episode)
650
651 def refresh_episodes(lctrl):
652 all_epis = [ epi for epi in emr.get_episodes(order_by = u'description') if epi.has_narrative ]
653 lctrl.set_string_items ([ [
654 u'%s%s' % (e['description'], gmTools.coalesce(e['health_issue'], u'', u' (%s)')),
655 gmTools.bool2subst(e['episode_open'], _('open'), _('closed'))
656 ] for e in all_epis
657 ])
658 lctrl.set_data(all_epis)
659
660 def get_episode_tooltip(episode):
661 return episode.format (
662 patient = pat,
663 with_encounters = False,
664 with_documents = False,
665 with_hospital_stays = False,
666 with_procedures = False,
667 with_family_history = False,
668 with_tests = False,
669 with_vaccinations = False
670 )
671
672
673
674 epis_picked_from = gmListWidgets.get_choices_from_list (
675 parent = parent,
676 msg = _('\n Select the episode you want to report on.'),
677 caption = _('Picking [%s] from episodes') % u'/'.join(i18n_soap_cats),
678 columns = [_('Episode'), _('Status')],
679 edit_callback = edit_episode,
680 refresh_callback = refresh_episodes,
681 single_selection = True,
682 can_return_empty = True,
683 ignore_OK_button = False,
684 left_extra_button = (
685 _('&Pick notes'),
686 _('Pick [%s] entries from selected episode') % u'/'.join(i18n_soap_cats),
687 pick_soap_from_episode
688 ),
689 list_tooltip_callback = get_episode_tooltip
690 )
691
692 if epis_picked_from is None:
693 return []
694
695 return selected_soap.values()
696
697
698
699
700
701
702
703
705 """soap_cats needs to be a list"""
706
707 pat = gmPerson.gmCurrentPatient()
708 emr = pat.get_emr()
709
710 if parent is None:
711 parent = wx.GetApp().GetTopWindow()
712
713 selected_soap = {}
714 selected_issue_pks = []
715 selected_episode_pks = []
716 selected_narrative_pks = []
717
718 while 1:
719
720 all_issues = emr.get_health_issues()
721 all_issues.insert(0, gmEMRStructItems.get_dummy_health_issue())
722 dlg = gmEMRStructWidgets.cIssueListSelectorDlg (
723 parent = parent,
724 id = -1,
725 issues = all_issues,
726 msg = _('\n In the list below mark the health issues you want to report on.\n')
727 )
728 selection_idxs = []
729 for idx in range(len(all_issues)):
730 if all_issues[idx]['pk_health_issue'] in selected_issue_pks:
731 selection_idxs.append(idx)
732 if len(selection_idxs) != 0:
733 dlg.set_selections(selections = selection_idxs)
734 btn_pressed = dlg.ShowModal()
735 selected_issues = dlg.get_selected_item_data()
736 dlg.Destroy()
737
738 if btn_pressed == wx.ID_CANCEL:
739 return selected_soap.values()
740
741 selected_issue_pks = [ i['pk_health_issue'] for i in selected_issues ]
742
743 while 1:
744
745 all_epis = emr.get_episodes(issues = selected_issue_pks)
746
747 if len(all_epis) == 0:
748 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.'))
749 break
750
751 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg (
752 parent = parent,
753 id = -1,
754 episodes = all_epis,
755 msg = _(
756 '\n These are the episodes known for the health issues just selected.\n\n'
757 ' Now, mark the the episodes you want to report on.\n'
758 )
759 )
760 selection_idxs = []
761 for idx in range(len(all_epis)):
762 if all_epis[idx]['pk_episode'] in selected_episode_pks:
763 selection_idxs.append(idx)
764 if len(selection_idxs) != 0:
765 dlg.set_selections(selections = selection_idxs)
766 btn_pressed = dlg.ShowModal()
767 selected_epis = dlg.get_selected_item_data()
768 dlg.Destroy()
769
770 if btn_pressed == wx.ID_CANCEL:
771 break
772
773 selected_episode_pks = [ i['pk_episode'] for i in selected_epis ]
774
775
776 all_narr = emr.get_clin_narrative(episodes = selected_episode_pks, soap_cats = soap_cats)
777
778 if len(all_narr) == 0:
779 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episodes.'))
780 continue
781
782 dlg = cNarrativeListSelectorDlg (
783 parent = parent,
784 id = -1,
785 narrative = all_narr,
786 msg = _(
787 '\n This is the narrative (type %s) for the chosen episodes.\n\n'
788 ' Now, mark the entries you want to include in your report.\n'
789 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soapu')) ])
790 )
791 selection_idxs = []
792 for idx in range(len(all_narr)):
793 if all_narr[idx]['pk_narrative'] in selected_narrative_pks:
794 selection_idxs.append(idx)
795 if len(selection_idxs) != 0:
796 dlg.set_selections(selections = selection_idxs)
797 btn_pressed = dlg.ShowModal()
798 selected_narr = dlg.get_selected_item_data()
799 dlg.Destroy()
800
801 if btn_pressed == wx.ID_CANCEL:
802 continue
803
804 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ]
805 for narr in selected_narr:
806 selected_soap[narr['pk_narrative']] = narr
807
809
811
812 narrative = kwargs['narrative']
813 del kwargs['narrative']
814
815 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs)
816
817 self.SetTitle(_('Select the narrative you are interested in ...'))
818
819 self._LCTRL_items.set_columns([_('when'), _('who'), _('type'), _('entry')])
820
821 self._LCTRL_items.set_string_items (
822 items = [ [narr['date'].strftime('%x %H:%M'), narr['provider'], gmClinNarrative.soap_cat2l10n[narr['soap_cat']], narr['narrative'].replace('\n', '/').replace('\r', '/')] for narr in narrative ]
823 )
824 self._LCTRL_items.set_column_widths()
825 self._LCTRL_items.set_data(data = narrative)
826
827 from Gnumed.wxGladeWidgets import wxgMoveNarrativeDlg
828
830
832
833 self.encounter = kwargs['encounter']
834 self.source_episode = kwargs['episode']
835 del kwargs['encounter']
836 del kwargs['episode']
837
838 wxgMoveNarrativeDlg.wxgMoveNarrativeDlg.__init__(self, *args, **kwargs)
839
840 self.LBL_source_episode.SetLabel(u'%s%s' % (self.source_episode['description'], gmTools.coalesce(self.source_episode['health_issue'], u'', u' (%s)')))
841 self.LBL_encounter.SetLabel('%s: %s %s - %s' % (
842 self.encounter['started'].strftime('%Y %b %d').decode(gmI18N.get_encoding()),
843 self.encounter['l10n_type'],
844 self.encounter['started'].strftime('%H:%M'),
845 self.encounter['last_affirmed'].strftime('%H:%M')
846 ))
847 pat = gmPerson.gmCurrentPatient()
848 emr = pat.get_emr()
849 narr = emr.get_clin_narrative(episodes=[self.source_episode['pk_episode']], encounters=[self.encounter['pk_encounter']])
850 if len(narr) == 0:
851 narr = [{'narrative': _('There is no narrative for this episode in this encounter.')}]
852 self.LBL_narrative.SetLabel(u'\n'.join([n['narrative'] for n in narr]))
853
854
876
877 from Gnumed.wxGladeWidgets import wxgSoapPluginPnl
878
879 -class cSoapPluginPnl(wxgSoapPluginPnl.wxgSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
880 """A panel for in-context editing of progress notes.
881
882 Expects to be used as a notebook page.
883
884 Left hand side:
885 - problem list (health issues and active episodes)
886 - previous notes
887
888 Right hand side:
889 - panel handling
890 - encounter details fields
891 - notebook with progress note editors
892 - visual progress notes
893
894 Listens to patient change signals, thus acts on the current patient.
895 """
905
906
907
909 self._LCTRL_active_problems.set_columns([_('Last'), _('Problem'), _('In health issue')])
910 self._LCTRL_active_problems.set_string_items()
911
912 self._splitter_main.SetSashGravity(0.5)
913 self._splitter_left.SetSashGravity(0.5)
914
915 splitter_size = self._splitter_main.GetSizeTuple()[0]
916 self._splitter_main.SetSashPosition(splitter_size * 3 / 10, True)
917
918 splitter_size = self._splitter_left.GetSizeTuple()[1]
919 self._splitter_left.SetSashPosition(splitter_size * 6 / 20, True)
920
922 """Clear all information from input panel."""
923
924 self._LCTRL_active_problems.set_string_items()
925
926 self._TCTRL_recent_notes.SetValue(u'')
927 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on selected problem'))
928
929 self._PNL_editors.patient = None
930
932 """Update health problems list."""
933
934 self._LCTRL_active_problems.set_string_items()
935
936 emr = self.__pat.get_emr()
937 problems = emr.get_problems (
938 include_closed_episodes = self._CHBOX_show_closed_episodes.IsChecked(),
939 include_irrelevant_issues = self._CHBOX_irrelevant_issues.IsChecked()
940 )
941
942 list_items = []
943 active_problems = []
944 for problem in problems:
945 if not problem['problem_active']:
946 if not problem['is_potential_problem']:
947 continue
948
949 active_problems.append(problem)
950
951 if problem['type'] == 'issue':
952 issue = emr.problem2issue(problem)
953 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue'])
954 if last_encounter is None:
955 last = issue['modified_when'].strftime('%m/%Y')
956 else:
957 last = last_encounter['last_affirmed'].strftime('%m/%Y')
958
959 list_items.append([last, problem['problem'], gmTools.u_left_arrow_with_tail])
960
961 elif problem['type'] == 'episode':
962 epi = emr.problem2episode(problem)
963 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode'])
964 if last_encounter is None:
965 last = epi['episode_modified_when'].strftime('%m/%Y')
966 else:
967 last = last_encounter['last_affirmed'].strftime('%m/%Y')
968
969 list_items.append ([
970 last,
971 problem['problem'],
972 gmTools.coalesce(initial = epi['health_issue'], instead = u'?')
973 ])
974
975 self._LCTRL_active_problems.set_string_items(items = list_items)
976 self._LCTRL_active_problems.set_column_widths()
977 self._LCTRL_active_problems.set_data(data = active_problems)
978
979 showing_potential_problems = (
980 self._CHBOX_show_closed_episodes.IsChecked()
981 or
982 self._CHBOX_irrelevant_issues.IsChecked()
983 )
984 if showing_potential_problems:
985 self._SZR_problem_list_staticbox.SetLabel(_('%s (active+potential) problems') % len(list_items))
986 else:
987 self._SZR_problem_list_staticbox.SetLabel(_('%s active problems') % len(list_items))
988
989 return True
990
992 soap = u''
993 emr = self.__pat.get_emr()
994 prev_enc = emr.get_last_but_one_encounter(issue_id = problem['pk_health_issue'])
995 if prev_enc is not None:
996 soap += prev_enc.format (
997 issues = [ problem['pk_health_issue'] ],
998 with_soap = True,
999 with_docs = fancy,
1000 with_tests = fancy,
1001 patient = self.__pat,
1002 fancy_header = False,
1003 with_rfe_aoe = True
1004 )
1005
1006 tmp = emr.active_encounter.format_soap (
1007 soap_cats = 'soapu',
1008 emr = emr,
1009 issues = [ problem['pk_health_issue'] ],
1010 )
1011 if len(tmp) > 0:
1012 soap += _('Current encounter:') + u'\n'
1013 soap += u'\n'.join(tmp) + u'\n'
1014
1015 if problem['summary'] is not None:
1016 soap += u'\n-- %s ----------\n%s' % (
1017 _('Cumulative summary'),
1018 gmTools.wrap (
1019 text = problem['summary'],
1020 width = 45,
1021 initial_indent = u' ',
1022 subsequent_indent = u' '
1023 ).strip('\n')
1024 )
1025
1026 return soap
1027
1029 soap = u''
1030 emr = self.__pat.get_emr()
1031 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_episode'])
1032 if prev_enc is not None:
1033 soap += prev_enc.format (
1034 episodes = [ problem['pk_episode'] ],
1035 with_soap = True,
1036 with_docs = fancy,
1037 with_tests = fancy,
1038 patient = self.__pat,
1039 fancy_header = False,
1040 with_rfe_aoe = True
1041 )
1042 else:
1043 if problem['pk_health_issue'] is not None:
1044 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_health_issue'])
1045 if prev_enc is not None:
1046 soap += prev_enc.format (
1047 with_soap = True,
1048 with_docs = fancy,
1049 with_tests = fancy,
1050 patient = self.__pat,
1051 issues = [ problem['pk_health_issue'] ],
1052 fancy_header = False,
1053 with_rfe_aoe = True
1054 )
1055
1056 tmp = emr.active_encounter.format_soap (
1057 soap_cats = 'soapu',
1058 emr = emr,
1059 issues = [ problem['pk_health_issue'] ],
1060 )
1061 if len(tmp) > 0:
1062 soap += _('Current encounter:') + u'\n'
1063 soap += u'\n'.join(tmp) + u'\n'
1064
1065 if problem['summary'] is not None:
1066 soap += u'\n-- %s ----------\n%s' % (
1067 _('Cumulative summary'),
1068 gmTools.wrap (
1069 text = problem['summary'],
1070 width = 45,
1071 initial_indent = u' ',
1072 subsequent_indent = u' '
1073 ).strip('\n')
1074 )
1075
1076 return soap
1077
1079 """This refreshes the recent-notes part."""
1080
1081 if problem is None:
1082 caption = u'<?>'
1083 txt = u''
1084 elif problem['type'] == u'issue':
1085 caption = problem['problem'][:35]
1086 txt = self.__get_info_for_issue_problem(problem = problem, fancy = not self._RBTN_notes_only.GetValue())
1087 elif problem['type'] == u'episode':
1088 caption = problem['problem'][:35]
1089 txt = self.__get_info_for_episode_problem(problem = problem, fancy = not self._RBTN_notes_only.GetValue())
1090
1091 self._TCTRL_recent_notes.SetValue(txt)
1092 self._TCTRL_recent_notes.ShowPosition(self._TCTRL_recent_notes.GetLastPosition())
1093 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent info on %s%s%s') % (
1094 gmTools.u_left_double_angle_quote,
1095 caption,
1096 gmTools.u_right_double_angle_quote
1097 ))
1098
1099 self._TCTRL_recent_notes.Refresh()
1100
1101 return True
1102
1103
1104
1106 """Configure enabled event signals."""
1107
1108 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1109 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1110 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db)
1111 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
1112 gmDispatcher.connect(signal = u'episode_code_mod_db', receiver = self._on_episode_issue_mod_db)
1113
1115 wx.CallAfter(self.__on_pre_patient_selection)
1116
1118 self.__reset_ui_content()
1119
1121 wx.CallAfter(self.__on_post_patient_selection)
1122
1124 self._schedule_data_reget()
1125 self._PNL_editors.patient = self.__pat
1126
1128 wx.CallAfter(self._schedule_data_reget)
1129
1130
1131
1133 """Show related note at the bottom."""
1134 pass
1135
1147
1149 """Show related note at the bottom."""
1150 self.__refresh_recent_notes (
1151 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1152 )
1153
1155 """Open progress note editor for this problem.
1156 """
1157 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1158 if problem is None:
1159 return True
1160
1161 dbcfg = gmCfg.cCfgSQL()
1162 allow_duplicate_editors = bool(dbcfg.get2 (
1163 option = u'horstspace.soap_editor.allow_same_episode_multiple_times',
1164 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1165 bias = u'user',
1166 default = False
1167 ))
1168 if self._PNL_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors):
1169 return True
1170
1171 gmGuiHelpers.gm_show_error (
1172 aMessage = _(
1173 'Cannot open progress note editor for\n\n'
1174 '[%s].\n\n'
1175 ) % problem['problem'],
1176 aTitle = _('opening progress note editor')
1177 )
1178 return False
1179
1181 self.__refresh_problem_list()
1182
1184 self.__refresh_problem_list()
1185
1186
1187
1189 self.__refresh_recent_notes (
1190 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1191 )
1192
1194 self.__refresh_recent_notes (
1195 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1196 )
1197
1198
1199
1200
1201
1202
1203
1205 self.__refresh_problem_list()
1206 return True
1207
1208
1209 from Gnumed.wxGladeWidgets import wxgFancySoapEditorPnl
1210
1212 """A panel holding everything needed to edit
1213
1214 - encounter metadata
1215 - textual progress notes
1216 - visual progress notes
1217
1218 in context. Does NOT act on the current patient.
1219 """
1227
1228
1229
1230 - def add_editor(self, problem=None, allow_same_problem=False):
1231 return self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_same_problem)
1232
1235
1237
1238
1239 self.__pat = patient
1240 self.__refresh_encounter()
1241 self.__refresh_soap_notebook()
1242
1243 patient = property(_get_patient, _set_patient)
1244
1246
1247 if self.__pat is None:
1248 return True
1249
1250 if not self.__encounter_valid_for_save():
1251 return False
1252
1253 enc = self.__pat.emr.active_encounter
1254
1255 rfe = self._TCTRL_rfe.GetValue().strip()
1256 if len(rfe) == 0:
1257 enc['reason_for_encounter'] = None
1258 else:
1259 enc['reason_for_encounter'] = rfe
1260 aoe = self._TCTRL_aoe.GetValue().strip()
1261 if len(aoe) == 0:
1262 enc['assessment_of_encounter'] = None
1263 else:
1264 enc['assessment_of_encounter'] = aoe
1265
1266 enc.save_payload()
1267
1268 enc.generic_codes_rfe = [ c['data'] for c in self._PRW_rfe_codes.GetData() ]
1269 enc.generic_codes_aoe = [ c['data'] for c in self._PRW_aoe_codes.GetData() ]
1270
1271 return True
1272
1273
1274
1276 self._NB_soap_editors.MoveAfterInTabOrder(self._PRW_aoe_codes)
1277
1279 self._NB_soap_editors.DeleteAllPages()
1280 self._NB_soap_editors.add_editor()
1281
1306
1308 self._TCTRL_rfe.SetValue(u'')
1309 self._PRW_rfe_codes.SetText(suppress_smarts = True)
1310 self._TCTRL_aoe.SetValue(u'')
1311 self._PRW_aoe_codes.SetText(suppress_smarts = True)
1312
1335
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1361
1362
1363
1365 """Configure enabled event signals."""
1366
1367 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
1368
1369
1370 gmDispatcher.connect(signal = u'doc_mod_db', receiver = self._on_doc_mod_db)
1371 gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified)
1372 gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_switched)
1373 gmDispatcher.connect(signal = u'rfe_code_mod_db', receiver = self._on_encounter_code_modified)
1374 gmDispatcher.connect(signal = u'aoe_code_mod_db', receiver = self._on_encounter_code_modified)
1375
1377 """Another patient is about to be activated.
1378
1379 Patient change will not proceed before this returns True.
1380 """
1381
1382
1383 if self.__pat is None:
1384 return True
1385 return self._NB_soap_editors.warn_on_unsaved_soap()
1386
1388 """The client is about to (be) shut down.
1389
1390 Shutdown will not proceed before this returns.
1391 """
1392 if self.__pat is None:
1393 return True
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409 saved = self._NB_soap_editors.save_all_editors (
1410 emr = self.__pat.emr,
1411 episode_name_candidates = [
1412 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
1413 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'')
1414 ]
1415 )
1416 if not saved:
1417 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
1418 return True
1419
1421 wx.CallAfter(self.__refresh_current_editor)
1422
1424 wx.CallAfter(self.__on_encounter_code_modified)
1425
1429
1431 wx.CallAfter(self.__refresh_encounter)
1432
1434 wx.CallAfter(self.__refresh_encounter)
1435
1436
1437
1441
1445
1449
1459
1479
1483
1484
1485
1489
1490
1491
1500
1512
1513
1704
1705
1706 from Gnumed.wxGladeWidgets import wxgSoapNoteExpandoEditAreaPnl
1707
1709 """An Edit Area like panel for entering progress notes.
1710
1711 Subjective: Codes:
1712 expando text ctrl
1713 Objective: Codes:
1714 expando text ctrl
1715 Assessment: Codes:
1716 expando text ctrl
1717 Plan: Codes:
1718 expando text ctrl
1719 visual progress notes
1720 panel with images
1721 Episode synopsis: Codes:
1722 text ctrl
1723
1724 - knows the problem this edit area is about
1725 - can deal with issue or episode type problems
1726 """
1727
1729
1730 try:
1731 self.problem = kwargs['problem']
1732 del kwargs['problem']
1733 except KeyError:
1734 self.problem = None
1735
1736 wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl.__init__(self, *args, **kwargs)
1737
1738 self.soap_fields = [
1739 self._TCTRL_Soap,
1740 self._TCTRL_sOap,
1741 self._TCTRL_soAp,
1742 self._TCTRL_soaP
1743 ]
1744
1745 self.__init_ui()
1746 self.__register_interests()
1747
1754
1758
1760 self._TCTRL_episode_summary.SetValue(u'')
1761 self._PRW_episode_codes.SetText(u'', self._PRW_episode_codes.list2data_dict([]))
1762 self._LBL_summary.SetLabel(_('Episode synopsis'))
1763
1764
1765 if self.problem is None:
1766 return
1767
1768
1769 if self.problem['type'] == u'issue':
1770 return
1771
1772
1773 caption = _(u'Synopsis (%s)') % (
1774 gmDateTime.pydt_strftime (
1775 self.problem['modified_when'],
1776 format = '%B %Y',
1777 accuracy = gmDateTime.acc_days
1778 )
1779 )
1780 self._LBL_summary.SetLabel(caption)
1781
1782 if self.problem['summary'] is not None:
1783 self._TCTRL_episode_summary.SetValue(self.problem['summary'].strip())
1784
1785 val, data = self._PRW_episode_codes.generic_linked_codes2item_dict(self.problem.generic_codes)
1786 self._PRW_episode_codes.SetText(val, data)
1787
1807
1809 for field in self.soap_fields:
1810 field.SetValue(u'')
1811 self._TCTRL_episode_summary.SetValue(u'')
1812 self._LBL_summary.SetLabel(_('Episode synopsis'))
1813 self._PRW_episode_codes.SetText(u'', self._PRW_episode_codes.list2data_dict([]))
1814 self._PNL_visual_soap.clear()
1815
1817 fname, discard_unmodified = select_visual_progress_note_template(parent = self)
1818 if fname is None:
1819 return False
1820
1821 if self.problem is None:
1822 issue = None
1823 episode = None
1824 elif self.problem['type'] == 'issue':
1825 issue = self.problem['pk_health_issue']
1826 episode = None
1827 else:
1828 issue = self.problem['pk_health_issue']
1829 episode = gmEMRStructItems.problem2episode(self.problem)
1830
1831 wx.CallAfter (
1832 edit_visual_progress_note,
1833 filename = fname,
1834 episode = episode,
1835 discard_unmodified = discard_unmodified,
1836 health_issue = issue
1837 )
1838
1839 - def save(self, emr=None, episode_name_candidates=None, encounter=None):
1840
1841 if self.empty:
1842 return True
1843
1844
1845 if (self.problem is None) or (self.problem['type'] == 'issue'):
1846 episode = self.__create_new_episode(emr = emr, episode_name_candidates = episode_name_candidates)
1847
1848 if episode is None:
1849 return False
1850
1851 else:
1852 episode = emr.problem2episode(self.problem)
1853
1854 if encounter is None:
1855 encounter = emr.current_encounter['pk_encounter']
1856
1857 soap_notes = []
1858 for note in self.soap:
1859 saved, data = gmClinNarrative.create_clin_narrative (
1860 soap_cat = note[0],
1861 narrative = note[1],
1862 episode_id = episode['pk_episode'],
1863 encounter_id = encounter
1864 )
1865 if saved:
1866 soap_notes.append(data)
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879 if self.problem is not None:
1880 if self.problem['type'] == 'episode':
1881 episode['summary'] = self._TCTRL_episode_summary.GetValue().strip()
1882 episode.save()
1883
1884
1885 episode.generic_codes = [ d['data'] for d in self._PRW_episode_codes.GetData() ]
1886
1887 return True
1888
1889
1890
1892
1893 episode_name_candidates.append(self._TCTRL_episode_summary.GetValue().strip())
1894 for candidate in episode_name_candidates:
1895 if candidate is None:
1896 continue
1897 epi_name = candidate.strip().replace('\r', '//').replace('\n', '//')
1898 break
1899
1900 dlg = wx.TextEntryDialog (
1901 parent = self,
1902 message = _('Enter a short working name for this new problem:'),
1903 caption = _('Creating a problem (episode) to save the notelet under ...'),
1904 defaultValue = epi_name,
1905 style = wx.OK | wx.CANCEL | wx.CENTRE
1906 )
1907 decision = dlg.ShowModal()
1908 if decision != wx.ID_OK:
1909 return None
1910
1911 epi_name = dlg.GetValue().strip()
1912 if epi_name == u'':
1913 gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note'))
1914 return None
1915
1916
1917 new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True)
1918 new_episode['summary'] = self._TCTRL_episode_summary.GetValue().strip()
1919 new_episode.save()
1920
1921 if self.problem is not None:
1922 issue = emr.problem2issue(self.problem)
1923 if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True):
1924 gmGuiHelpers.gm_show_warning (
1925 _(
1926 'The new episode:\n'
1927 '\n'
1928 ' "%s"\n'
1929 '\n'
1930 'will remain unassociated despite the editor\n'
1931 'having been invoked from the health issue:\n'
1932 '\n'
1933 ' "%s"'
1934 ) % (
1935 new_episode['description'],
1936 issue['description']
1937 ),
1938 _('saving progress note')
1939 )
1940
1941 return new_episode
1942
1943
1944
1946 for field in self.soap_fields:
1947 wx_expando.EVT_ETC_LAYOUT_NEEDED(field, field.GetId(), self._on_expando_needs_layout)
1948 wx_expando.EVT_ETC_LAYOUT_NEEDED(self._TCTRL_episode_summary, self._TCTRL_episode_summary.GetId(), self._on_expando_needs_layout)
1949 gmDispatcher.connect(signal = u'doc_page_mod_db', receiver = self._refresh_visual_soap)
1950
1953
1955
1956
1957
1958
1959 self.FitInside()
1960
1961 if self.HasScrollbar(wx.VERTICAL):
1962
1963 expando = self.FindWindowById(evt.GetId())
1964 y_expando = expando.GetPositionTuple()[1]
1965 h_expando = expando.GetSizeTuple()[1]
1966 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1
1967 if expando.NumberOfLines == 0:
1968 no_of_lines = 1
1969 else:
1970 no_of_lines = expando.NumberOfLines
1971 y_cursor = int(round((float(line_cursor) / no_of_lines) * h_expando))
1972 y_desired_visible = y_expando + y_cursor
1973
1974 y_view = self.ViewStart[1]
1975 h_view = self.GetClientSizeTuple()[1]
1976
1977
1978
1979
1980
1981
1982
1983
1984 if y_desired_visible < y_view:
1985
1986 self.Scroll(0, y_desired_visible)
1987
1988 if y_desired_visible > h_view:
1989
1990 self.Scroll(0, y_desired_visible)
1991
1992
1993
1995 soap_notes = []
1996
1997 tmp = self._TCTRL_Soap.GetValue().strip()
1998 if tmp != u'':
1999 soap_notes.append(['s', tmp])
2000
2001 tmp = self._TCTRL_sOap.GetValue().strip()
2002 if tmp != u'':
2003 soap_notes.append(['o', tmp])
2004
2005 tmp = self._TCTRL_soAp.GetValue().strip()
2006 if tmp != u'':
2007 soap_notes.append(['a', tmp])
2008
2009 tmp = self._TCTRL_soaP.GetValue().strip()
2010 if tmp != u'':
2011 soap_notes.append(['p', tmp])
2012
2013 return soap_notes
2014
2015 soap = property(_get_soap, lambda x:x)
2016
2018
2019
2020 for field in self.soap_fields:
2021 if field.GetValue().strip() != u'':
2022 return False
2023
2024
2025 summary = self._TCTRL_episode_summary.GetValue().strip()
2026 if self.problem is None:
2027 if summary != u'':
2028 return False
2029 elif self.problem['type'] == u'issue':
2030 if summary != u'':
2031 return False
2032 else:
2033 if self.problem['summary'] is None:
2034 if summary != u'':
2035 return False
2036 else:
2037 if summary != self.problem['summary'].strip():
2038 return False
2039
2040
2041 new_codes = self._PRW_episode_codes.GetData()
2042 if self.problem is None:
2043 if len(new_codes) > 0:
2044 return False
2045 elif self.problem['type'] == u'issue':
2046 if len(new_codes) > 0:
2047 return False
2048 else:
2049 old_code_pks = self.problem.generic_codes
2050 if len(old_code_pks) != len(new_codes):
2051 return False
2052 for code in new_codes:
2053 if code['data'] not in old_code_pks:
2054 return False
2055
2056 return True
2057
2058 empty = property(_get_empty, lambda x:x)
2059
2060 -class cSoapLineTextCtrl(wx_expando.ExpandoTextCtrl, gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin):
2061
2062 - def __init__(self, *args, **kwargs):
2063
2064 wx_expando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
2065 gmKeywordExpansionWidgets.cKeywordExpansion_TextCtrlMixin.__init__(self)
2066 self.enable_keyword_expansions()
2067
2068 self.__register_interests()
2069
2070
2071
2072 - def _wrapLine(self, line, dc, width):
2073
2074 if (wx.MAJOR_VERSION >= 2) and (wx.MINOR_VERSION > 8):
2075 return wx_expando.ExpandoTextCtrl._wrapLine(line, dc, width)
2076
2077
2078
2079
2080 pte = dc.GetPartialTextExtents(line)
2081 width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X)
2082 idx = 0
2083 start = 0
2084 count = 0
2085 spc = -1
2086 while idx < len(pte):
2087 if line[idx] == ' ':
2088 spc = idx
2089 if pte[idx] - start > width:
2090
2091 count += 1
2092
2093 if spc != -1:
2094 idx = spc + 1
2095 spc = -1
2096 if idx < len(pte):
2097 start = pte[idx]
2098 else:
2099 idx += 1
2100 return count
2101
2102
2103
2105
2106
2107 wx.EVT_SET_FOCUS(self, self.__on_focus)
2108
2109 - def __on_focus(self, evt):
2110 evt.Skip()
2111 wx.CallAfter(self._after_on_focus)
2112
2113 - def _after_on_focus(self):
2114
2115
2116
2117
2118 if not self:
2119 return
2120
2121 evt = wx.PyCommandEvent(wx_expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
2122 evt.SetEventObject(self)
2123
2124
2125
2126
2127 self.GetEventHandler().ProcessEvent(evt)
2128
2129
2130
2131
2162
2163 cmd = gmCfgWidgets.configure_string_option (
2164 message = _(
2165 'Enter the shell command with which to start\n'
2166 'the image editor for visual progress notes.\n'
2167 '\n'
2168 'Any "%(img)s" included with the arguments\n'
2169 'will be replaced by the file name of the\n'
2170 'note template.'
2171 ),
2172 option = u'external.tools.visual_soap_editor_cmd',
2173 bias = 'user',
2174 default_value = None,
2175 validator = is_valid
2176 )
2177
2178 return cmd
2179
2181 if parent is None:
2182 parent = wx.GetApp().GetTopWindow()
2183
2184 dlg = wx.FileDialog (
2185 parent = parent,
2186 message = _('Choose file to use as template for new visual progress note'),
2187 defaultDir = os.path.expanduser('~'),
2188 defaultFile = '',
2189
2190 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST
2191 )
2192 result = dlg.ShowModal()
2193
2194 if result == wx.ID_CANCEL:
2195 dlg.Destroy()
2196 return None
2197
2198 full_filename = dlg.GetPath()
2199 dlg.Hide()
2200 dlg.Destroy()
2201 return full_filename
2202
2204
2205 if parent is None:
2206 parent = wx.GetApp().GetTopWindow()
2207
2208 dlg = gmGuiHelpers.c3ButtonQuestionDlg (
2209 parent,
2210 -1,
2211 caption = _('Visual progress note source'),
2212 question = _('From which source do you want to pick the image template ?'),
2213 button_defs = [
2214 {'label': _('Database'), 'tooltip': _('List of templates in the database.'), 'default': True},
2215 {'label': _('File'), 'tooltip': _('Files in the filesystem.'), 'default': False},
2216 {'label': _('Device'), 'tooltip': _('Image capture devices (scanners, cameras, etc)'), 'default': False}
2217 ]
2218 )
2219 result = dlg.ShowModal()
2220 dlg.Destroy()
2221
2222
2223 if result == wx.ID_YES:
2224 _log.debug('visual progress note template from: database template')
2225 from Gnumed.wxpython import gmFormWidgets
2226 template = gmFormWidgets.manage_form_templates (
2227 parent = parent,
2228 template_types = [gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE],
2229 active_only = True
2230 )
2231 if template is None:
2232 return (None, None)
2233 filename = template.export_to_file()
2234 if filename is None:
2235 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note template for [%s].') % template['name_long'])
2236 return (None, None)
2237 return (filename, True)
2238
2239
2240 if result == wx.ID_NO:
2241 _log.debug('visual progress note template from: disk file')
2242 fname = select_file_as_visual_progress_note_template(parent = parent)
2243 if fname is None:
2244 return (None, None)
2245
2246 ext = os.path.splitext(fname)[1]
2247 tmp_name = gmTools.get_unique_filename(suffix = ext)
2248 _log.debug('visual progress note from file: [%s] -> [%s]', fname, tmp_name)
2249 shutil.copy2(fname, tmp_name)
2250 return (tmp_name, False)
2251
2252
2253 if result == wx.ID_CANCEL:
2254 _log.debug('visual progress note template from: image capture device')
2255 fnames = gmDocumentWidgets.acquire_images_from_capture_device(device = None, calling_window = parent)
2256 if fnames is None:
2257 return (None, None)
2258 if len(fnames) == 0:
2259 return (None, None)
2260 return (fnames[0], False)
2261
2262 _log.debug('no visual progress note template source selected')
2263 return (None, None)
2264
2266 """This assumes <filename> contains an image which can be handled by the configured image editor."""
2267
2268 if doc_part is not None:
2269 filename = doc_part.export_to_file()
2270 if filename is None:
2271 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.'))
2272 return None
2273
2274 dbcfg = gmCfg.cCfgSQL()
2275 cmd = dbcfg.get2 (
2276 option = u'external.tools.visual_soap_editor_cmd',
2277 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2278 bias = 'user'
2279 )
2280
2281 if cmd is None:
2282 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = False)
2283 cmd = configure_visual_progress_note_editor()
2284 if cmd is None:
2285 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = True)
2286 return None
2287
2288 if u'%(img)s' in cmd:
2289 cmd = cmd % {u'img': filename}
2290 else:
2291 cmd = u'%s %s' % (cmd, filename)
2292
2293 if discard_unmodified:
2294 original_stat = os.stat(filename)
2295 original_md5 = gmTools.file2md5(filename)
2296
2297 success = gmShellAPI.run_command_in_shell(cmd, blocking = True)
2298 if not success:
2299 gmGuiHelpers.gm_show_error (
2300 _(
2301 'There was a problem with running the editor\n'
2302 'for visual progress notes.\n'
2303 '\n'
2304 ' [%s]\n'
2305 '\n'
2306 ) % cmd,
2307 _('Editing visual progress note')
2308 )
2309 return None
2310
2311 try:
2312 open(filename, 'r').close()
2313 except StandardError:
2314 _log.exception('problem accessing visual progress note file [%s]', filename)
2315 gmGuiHelpers.gm_show_error (
2316 _(
2317 'There was a problem reading the visual\n'
2318 'progress note from the file:\n'
2319 '\n'
2320 ' [%s]\n'
2321 '\n'
2322 ) % filename,
2323 _('Saving visual progress note')
2324 )
2325 return None
2326
2327 if discard_unmodified:
2328 modified_stat = os.stat(filename)
2329
2330 if original_stat.st_size == modified_stat.st_size:
2331 modified_md5 = gmTools.file2md5(filename)
2332
2333 if original_md5 == modified_md5:
2334 _log.debug('visual progress note (template) not modified')
2335
2336 msg = _(
2337 u'You either created a visual progress note from a template\n'
2338 u'in the database (rather than from a file on disk) or you\n'
2339 u'edited an existing visual progress note.\n'
2340 u'\n'
2341 u'The template/original was not modified at all, however.\n'
2342 u'\n'
2343 u'Do you still want to save the unmodified image as a\n'
2344 u'visual progress note into the EMR of the patient ?\n'
2345 )
2346 save_unmodified = gmGuiHelpers.gm_show_question (
2347 msg,
2348 _('Saving visual progress note')
2349 )
2350 if not save_unmodified:
2351 _log.debug('user discarded unmodified note')
2352 return
2353
2354 if doc_part is not None:
2355 _log.debug('updating visual progress note')
2356 doc_part.update_data_from_file(fname = filename)
2357 doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2358 return None
2359
2360 if not isinstance(episode, gmEMRStructItems.cEpisode):
2361 if episode is None:
2362 episode = _('visual progress notes')
2363 pat = gmPerson.gmCurrentPatient()
2364 emr = pat.get_emr()
2365 episode = emr.add_episode(episode_name = episode.strip(), pk_health_issue = health_issue, is_open = False)
2366
2367 doc = gmDocumentWidgets.save_file_as_new_document (
2368 filename = filename,
2369 document_type = gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE,
2370 episode = episode,
2371 unlock_patient = False
2372 )
2373 doc.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2374
2375 return doc
2376
2378 """Phrasewheel to allow selection of visual SOAP template."""
2379
2381
2382 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
2383
2384 query = u"""
2385 SELECT
2386 pk AS data,
2387 name_short AS list_label,
2388 name_sort AS field_label
2389 FROM
2390 ref.paperwork_templates
2391 WHERE
2392 fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND (
2393 name_long %%(fragment_condition)s
2394 OR
2395 name_short %%(fragment_condition)s
2396 )
2397 ORDER BY list_label
2398 LIMIT 15
2399 """ % gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE
2400
2401 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
2402 mp.setThresholds(2, 3, 5)
2403
2404 self.matcher = mp
2405 self.selection_only = True
2406
2412
2413 from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl
2414
2416
2421
2422
2423
2424 - def refresh(self, document_folder=None, episodes=None, encounter=None):
2425
2426 self.clear()
2427 if document_folder is not None:
2428 soap_docs = document_folder.get_visual_progress_notes(episodes = episodes, encounter = encounter)
2429 if len(soap_docs) > 0:
2430 for soap_doc in soap_docs:
2431 parts = soap_doc.parts
2432 if len(parts) == 0:
2433 continue
2434 part = parts[0]
2435 fname = part.export_to_file()
2436 if fname is None:
2437 continue
2438
2439
2440 img = gmGuiHelpers.file2scaled_image (
2441 filename = fname,
2442 height = 30
2443 )
2444
2445 bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER)
2446
2447
2448 img = gmGuiHelpers.file2scaled_image (
2449 filename = fname,
2450 height = 150
2451 )
2452 tip = agw_stt.SuperToolTip (
2453 u'',
2454 bodyImage = img,
2455 header = _('Created: %s') % part['date_generated'].strftime('%Y %B %d').decode(gmI18N.get_encoding()),
2456 footer = gmTools.coalesce(part['doc_comment'], u'').strip()
2457 )
2458 tip.SetTopGradientColor('white')
2459 tip.SetMiddleGradientColor('white')
2460 tip.SetBottomGradientColor('white')
2461 tip.SetTarget(bmp)
2462
2463 bmp.doc_part = part
2464 bmp.Bind(wx.EVT_LEFT_UP, self._on_bitmap_leftclicked)
2465
2466 self._SZR_soap.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM | wx.EXPAND, 3)
2467 self.__bitmaps.append(bmp)
2468
2469 self.GetParent().Layout()
2470
2472 while len(self._SZR_soap.GetChildren()) > 0:
2473 self._SZR_soap.Detach(0)
2474
2475
2476 for bmp in self.__bitmaps:
2477 bmp.Destroy()
2478 self.__bitmaps = []
2479
2481 wx.CallAfter (
2482 edit_visual_progress_note,
2483 doc_part = evt.GetEventObject().doc_part,
2484 discard_unmodified = True
2485 )
2486
2487
2488
2489 from Gnumed.wxGladeWidgets import wxgSimpleSoapPluginPnl
2490
2491 -class cSimpleSoapPluginPnl(wxgSimpleSoapPluginPnl.wxgSimpleSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
2501
2502
2503
2505 self._LCTRL_problems.set_columns(columns = [_('Problem list')])
2506 self._LCTRL_problems.activate_callback = self._on_problem_activated
2507 self._LCTRL_problems.item_tooltip_callback = self._on_get_problem_tooltip
2508
2509 self._splitter_main.SetSashGravity(0.5)
2510 splitter_width = self._splitter_main.GetSizeTuple()[0]
2511 self._splitter_main.SetSashPosition(splitter_width / 2, True)
2512
2513 self._TCTRL_soap.Disable()
2514 self._BTN_save_soap.Disable()
2515 self._BTN_clear_soap.Disable()
2516
2518 self._LCTRL_problems.set_string_items()
2519 self._TCTRL_soap_problem.SetValue(_('<above, double-click problem to start entering SOAP note>'))
2520 self._TCTRL_soap.SetValue(u'')
2521 self._CHBOX_filter_by_problem.SetLabel(_('&Filter by problem'))
2522 self._TCTRL_journal.SetValue(u'')
2523
2524 self._TCTRL_soap.Disable()
2525 self._BTN_save_soap.Disable()
2526 self._BTN_clear_soap.Disable()
2527
2529 if not self.__curr_pat.connected:
2530 return None
2531
2532 if self.__problem is None:
2533 return None
2534
2535 saved = self.__curr_pat.emr.add_clin_narrative (
2536 note = self._TCTRL_soap.GetValue().strip(),
2537 soap_cat = u'u',
2538 episode = self.__problem
2539 )
2540
2541 if saved is None:
2542 return False
2543
2544 self._TCTRL_soap.SetValue(u'')
2545 self.__refresh_journal()
2546 return True
2547
2549 if self._TCTRL_soap.GetValue().strip() == u'':
2550 return True
2551 if self.__problem is None:
2552
2553 self._TCTRL_soap.SetValue(u'')
2554 return None
2555 save_it = gmGuiHelpers.gm_show_question (
2556 title = _('Saving SOAP note'),
2557 question = _('Do you want to save the SOAP note ?')
2558 )
2559 if save_it:
2560 return self.__save_soap()
2561 return False
2562
2573
2594
2595
2596
2598 """Configure enabled event signals."""
2599
2600 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
2601 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
2602 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db)
2603 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
2604
2605
2606 self.__curr_pat.register_pre_selection_callback(callback = self._pre_selection_callback)
2607 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
2608
2610 """Another patient is about to be activated.
2611
2612 Patient change will not proceed before this returns True.
2613 """
2614 if not self.__curr_pat.connected:
2615 return True
2616 self.__perhaps_save_soap()
2617 self.__problem = None
2618 return True
2619
2621 """The client is about to be shut down.
2622
2623 Shutdown will not proceed before this returns.
2624 """
2625 if not self.__curr_pat.connected:
2626 return
2627 if not self.__save_soap():
2628 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save SimpleNotes SOAP note.'), beep = True)
2629 return
2630
2632 wx.CallAfter(self.__reset_ui)
2633
2635 wx.CallAfter(self._schedule_data_reget)
2636
2638 wx.CallAfter(self._schedule_data_reget)
2639
2641 self.__perhaps_save_soap()
2642 epi = self._LCTRL_problems.get_selected_item_data(only_one = True)
2643 self._TCTRL_soap_problem.SetValue(_('Progress note: %s%s') % (
2644 epi['description'],
2645 gmTools.coalesce(epi['health_issue'], u'', u' (%s)')
2646 ))
2647 self.__problem = epi
2648 self._TCTRL_soap.SetValue(u'')
2649
2650 self._TCTRL_soap.Enable()
2651 self._BTN_save_soap.Enable()
2652 self._BTN_clear_soap.Enable()
2653
2668
2670 event.Skip()
2671 self.__refresh_journal()
2672
2674 event.Skip()
2675 self.__refresh_journal()
2676
2691
2698
2706
2710
2714
2715
2716
2718 self.__refresh_problem_list()
2719 self.__refresh_journal()
2720 self._TCTRL_soap.SetValue(u'')
2721 return True
2722
2723
2724
2725
2726 if __name__ == '__main__':
2727
2728 if len(sys.argv) < 2:
2729 sys.exit()
2730
2731 if sys.argv[1] != 'test':
2732 sys.exit()
2733
2734 gmI18N.activate_locale()
2735 gmI18N.install_domain(domain = 'gnumed')
2736
2737
2746
2753
2766
2767 test_select_narrative_from_episodes()
2768
2769
2770
2771
2772