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