1 """GNUmed narrative handling widgets."""
2
3 __version__ = "$Revision: 1.46 $"
4 __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
5
6 import sys, logging, os, os.path, time, re as regex, shutil
7
8
9 import wx
10 import wx.lib.expando as wxexpando
11
12
13 if __name__ == '__main__':
14 sys.path.insert(0, '../../')
15 from Gnumed.pycommon import gmI18N, gmDispatcher, gmTools, gmDateTime
16 from Gnumed.pycommon import gmShellAPI, gmPG2, gmCfg, gmMatchProvider
17 from Gnumed.business import gmPerson, gmEMRStructItems, gmClinNarrative, gmSurgery
18 from Gnumed.business import gmForms, gmDocuments
19 from Gnumed.wxpython import gmListWidgets, gmEMRStructWidgets, gmRegetMixin
20 from Gnumed.wxpython import gmPhraseWheel, gmGuiHelpers, gmPatSearchWidgets
21 from Gnumed.wxpython import gmCfgWidgets, gmDocumentWidgets
22 from Gnumed.exporters import gmPatientExporter
23
24
25 _log = logging.getLogger('gm.ui')
26 _log.info(__version__)
27
28
29
31
32
33 if patient is None:
34 patient = gmPerson.gmCurrentPatient()
35
36 if not patient.connected:
37 gmDispatcher.send(signal = 'statustext', msg = _('Cannot move progress notes. No active patient.'))
38 return False
39
40 if parent is None:
41 parent = wx.GetApp().GetTopWindow()
42
43 emr = patient.get_emr()
44
45 if encounters is None:
46 encs = emr.get_encounters(episodes = episodes)
47 encounters = gmEMRStructWidgets.select_encounters (
48 parent = parent,
49 patient = patient,
50 single_selection = False,
51 encounters = encs
52 )
53
54 notes = emr.get_clin_narrative (
55 encounters = encounters,
56 episodes = episodes
57 )
58
59
60 if move_all:
61 selected_narr = notes
62 else:
63 selected_narr = gmListWidgets.get_choices_from_list (
64 parent = parent,
65 caption = _('Moving progress notes between encounters ...'),
66 single_selection = False,
67 can_return_empty = True,
68 data = notes,
69 msg = _('\n Select the progress notes to move from the list !\n\n'),
70 columns = [_('when'), _('who'), _('type'), _('entry')],
71 choices = [
72 [ narr['date'].strftime('%x %H:%M'),
73 narr['provider'],
74 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
75 narr['narrative'].replace('\n', '/').replace('\r', '/')
76 ] for narr in notes
77 ]
78 )
79
80 if not selected_narr:
81 return True
82
83
84 enc2move2 = gmEMRStructWidgets.select_encounters (
85 parent = parent,
86 patient = patient,
87 single_selection = True
88 )
89
90 if not enc2move2:
91 return True
92
93 for narr in selected_narr:
94 narr['pk_encounter'] = enc2move2['pk_encounter']
95 narr.save()
96
97 return True
98
100
101
102 if patient is None:
103 patient = gmPerson.gmCurrentPatient()
104
105 if not patient.connected:
106 gmDispatcher.send(signal = 'statustext', msg = _('Cannot edit progress notes. No active patient.'))
107 return False
108
109 if parent is None:
110 parent = wx.GetApp().GetTopWindow()
111
112 emr = patient.get_emr()
113
114 def delete(item):
115 if item is None:
116 return False
117 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
118 parent,
119 -1,
120 caption = _('Deleting progress note'),
121 question = _(
122 'Are you positively sure you want to delete this\n'
123 'progress note from the medical record ?\n'
124 '\n'
125 'Note that even if you chose to delete the entry it will\n'
126 'still be (invisibly) kept in the audit trail to protect\n'
127 'you from litigation because physical deletion is known\n'
128 'to be unlawful in some jurisdictions.\n'
129 ),
130 button_defs = (
131 {'label': _('Delete'), 'tooltip': _('Yes, delete the progress note.'), 'default': False},
132 {'label': _('Cancel'), 'tooltip': _('No, do NOT delete the progress note.'), 'default': True}
133 )
134 )
135 decision = dlg.ShowModal()
136
137 if decision != wx.ID_YES:
138 return False
139
140 gmClinNarrative.delete_clin_narrative(narrative = item['pk_narrative'])
141 return True
142
143 def edit(item):
144 if item is None:
145 return False
146
147 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
148 parent,
149 -1,
150 title = _('Editing progress note'),
151 msg = _('This is the original progress note:'),
152 data = item.format(left_margin = u' ', fancy = True),
153 text = item['narrative']
154 )
155 decision = dlg.ShowModal()
156
157 if decision != wx.ID_SAVE:
158 return False
159
160 val = dlg.value
161 dlg.Destroy()
162 if val.strip() == u'':
163 return False
164
165 item['narrative'] = val
166 item.save_payload()
167
168 return True
169
170 def refresh(lctrl):
171 notes = emr.get_clin_narrative (
172 encounters = encounters,
173 episodes = episodes,
174 providers = [ gmPerson.gmCurrentProvider()['short_alias'] ]
175 )
176 lctrl.set_string_items(items = [
177 [ narr['date'].strftime('%x %H:%M'),
178 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
179 narr['narrative'].replace('\n', '/').replace('\r', '/')
180 ] for narr in notes
181 ])
182 lctrl.set_data(data = notes)
183
184
185 gmListWidgets.get_choices_from_list (
186 parent = parent,
187 caption = _('Managing progress notes'),
188 msg = _(
189 '\n'
190 ' This list shows the progress notes by %s.\n'
191 '\n'
192 ) % gmPerson.gmCurrentProvider()['short_alias'],
193 columns = [_('when'), _('type'), _('entry')],
194 single_selection = True,
195 can_return_empty = False,
196 edit_callback = edit,
197 delete_callback = delete,
198 refresh_callback = refresh,
199 ignore_OK_button = True
200 )
201
203
204 if parent is None:
205 parent = wx.GetApp().GetTopWindow()
206
207 searcher = wx.TextEntryDialog (
208 parent = parent,
209 message = _('Enter (regex) term to search for across all EMRs:'),
210 caption = _('Text search across all EMRs'),
211 style = wx.OK | wx.CANCEL | wx.CENTRE
212 )
213 result = searcher.ShowModal()
214
215 if result != wx.ID_OK:
216 return
217
218 wx.BeginBusyCursor()
219 term = searcher.GetValue()
220 searcher.Destroy()
221 results = gmClinNarrative.search_text_across_emrs(search_term = term)
222 wx.EndBusyCursor()
223
224 if len(results) == 0:
225 gmGuiHelpers.gm_show_info (
226 _(
227 'Nothing found for search term:\n'
228 ' "%s"'
229 ) % term,
230 _('Search results')
231 )
232 return
233
234 items = [ [gmPerson.cIdentity(aPK_obj = r['pk_patient'])['description_gender'], r['narrative'], r['src_table']] for r in results ]
235
236 selected_patient = gmListWidgets.get_choices_from_list (
237 parent = parent,
238 caption = _('Search results for %s') % term,
239 choices = items,
240 columns = [_('Patient'), _('Match'), _('Match location')],
241 data = [ r['pk_patient'] for r in results ],
242 single_selection = True,
243 can_return_empty = False
244 )
245
246 if selected_patient is None:
247 return
248
249 wx.CallAfter(gmPatSearchWidgets.set_active_patient, patient = gmPerson.cIdentity(aPK_obj = selected_patient))
250
252
253
254 if patient is None:
255 patient = gmPerson.gmCurrentPatient()
256
257 if not patient.connected:
258 gmDispatcher.send(signal = 'statustext', msg = _('Cannot search EMR. No active patient.'))
259 return False
260
261 if parent is None:
262 parent = wx.GetApp().GetTopWindow()
263
264 searcher = wx.TextEntryDialog (
265 parent = parent,
266 message = _('Enter search term:'),
267 caption = _('Text search of entire EMR of active patient'),
268 style = wx.OK | wx.CANCEL | wx.CENTRE
269 )
270 result = searcher.ShowModal()
271
272 if result != wx.ID_OK:
273 searcher.Destroy()
274 return False
275
276 wx.BeginBusyCursor()
277 val = searcher.GetValue()
278 searcher.Destroy()
279 emr = patient.get_emr()
280 rows = emr.search_narrative_simple(val)
281 wx.EndBusyCursor()
282
283 if len(rows) == 0:
284 gmGuiHelpers.gm_show_info (
285 _(
286 'Nothing found for search term:\n'
287 ' "%s"'
288 ) % val,
289 _('Search results')
290 )
291 return True
292
293 txt = u''
294 for row in rows:
295 txt += u'%s: %s\n' % (
296 row['soap_cat'],
297 row['narrative']
298 )
299
300 txt += u' %s: %s - %s %s\n' % (
301 _('Encounter'),
302 row['encounter_started'].strftime('%x %H:%M'),
303 row['encounter_ended'].strftime('%H:%M'),
304 row['encounter_type']
305 )
306 txt += u' %s: %s\n' % (
307 _('Episode'),
308 row['episode']
309 )
310 txt += u' %s: %s\n\n' % (
311 _('Health issue'),
312 row['health_issue']
313 )
314
315 msg = _(
316 'Search term was: "%s"\n'
317 '\n'
318 'Search results:\n\n'
319 '%s\n'
320 ) % (val, txt)
321
322 dlg = wx.MessageDialog (
323 parent = parent,
324 message = msg,
325 caption = _('Search results for %s') % val,
326 style = wx.OK | wx.STAY_ON_TOP
327 )
328 dlg.ShowModal()
329 dlg.Destroy()
330
331 return True
332
334
335
336 pat = gmPerson.gmCurrentPatient()
337 if not pat.connected:
338 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR for Medistar. No active patient.'))
339 return False
340
341 if encounter is None:
342 encounter = pat.get_emr().active_encounter
343
344 if parent is None:
345 parent = wx.GetApp().GetTopWindow()
346
347
348 aWildcard = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files"))
349
350 aDefDir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed','export')))
351
352 fname = '%s-%s-%s-%s-%s.txt' % (
353 'Medistar-MD',
354 time.strftime('%Y-%m-%d',time.localtime()),
355 pat['lastnames'].replace(' ', '-'),
356 pat['firstnames'].replace(' ', '_'),
357 pat.get_formatted_dob(format = '%Y-%m-%d')
358 )
359 dlg = wx.FileDialog (
360 parent = parent,
361 message = _("Save EMR extract for MEDISTAR import as..."),
362 defaultDir = aDefDir,
363 defaultFile = fname,
364 wildcard = aWildcard,
365 style = wx.SAVE
366 )
367 choice = dlg.ShowModal()
368 fname = dlg.GetPath()
369 dlg.Destroy()
370 if choice != wx.ID_OK:
371 return False
372
373 wx.BeginBusyCursor()
374 _log.debug('exporting encounter for medistar import to [%s]', fname)
375 exporter = gmPatientExporter.cMedistarSOAPExporter()
376 successful, fname = exporter.export_to_file (
377 filename = fname,
378 encounter = encounter,
379 soap_cats = u'soap',
380 export_to_import_file = True
381 )
382 if not successful:
383 gmGuiHelpers.gm_show_error (
384 _('Error exporting progress notes for MEDISTAR import.'),
385 _('MEDISTAR progress notes export')
386 )
387 wx.EndBusyCursor()
388 return False
389
390 gmDispatcher.send(signal = 'statustext', msg = _('Successfully exported progress notes into file [%s] for Medistar import.') % fname, beep=False)
391
392 wx.EndBusyCursor()
393 return True
394
396 """soap_cats needs to be a list"""
397
398 pat = gmPerson.gmCurrentPatient()
399 emr = pat.get_emr()
400
401 if parent is None:
402 parent = wx.GetApp().GetTopWindow()
403
404 selected_soap = {}
405 selected_issue_pks = []
406 selected_episode_pks = []
407 selected_narrative_pks = []
408
409 while 1:
410
411 all_issues = emr.get_health_issues()
412 all_issues.insert(0, gmEMRStructItems.get_dummy_health_issue())
413 dlg = gmEMRStructWidgets.cIssueListSelectorDlg (
414 parent = parent,
415 id = -1,
416 issues = all_issues,
417 msg = _('\n In the list below mark the health issues you want to report on.\n')
418 )
419 selection_idxs = []
420 for idx in range(len(all_issues)):
421 if all_issues[idx]['pk_health_issue'] in selected_issue_pks:
422 selection_idxs.append(idx)
423 if len(selection_idxs) != 0:
424 dlg.set_selections(selections = selection_idxs)
425 btn_pressed = dlg.ShowModal()
426 selected_issues = dlg.get_selected_item_data()
427 dlg.Destroy()
428
429 if btn_pressed == wx.ID_CANCEL:
430 return selected_soap.values()
431
432 selected_issue_pks = [ i['pk_health_issue'] for i in selected_issues ]
433
434 while 1:
435
436 all_epis = emr.get_episodes(issues = selected_issue_pks)
437
438 if len(all_epis) == 0:
439 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.'))
440 break
441
442 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg (
443 parent = parent,
444 id = -1,
445 episodes = all_epis,
446 msg = _(
447 '\n These are the episodes known for the health issues just selected.\n\n'
448 ' Now, mark the the episodes you want to report on.\n'
449 )
450 )
451 selection_idxs = []
452 for idx in range(len(all_epis)):
453 if all_epis[idx]['pk_episode'] in selected_episode_pks:
454 selection_idxs.append(idx)
455 if len(selection_idxs) != 0:
456 dlg.set_selections(selections = selection_idxs)
457 btn_pressed = dlg.ShowModal()
458 selected_epis = dlg.get_selected_item_data()
459 dlg.Destroy()
460
461 if btn_pressed == wx.ID_CANCEL:
462 break
463
464 selected_episode_pks = [ i['pk_episode'] for i in selected_epis ]
465
466
467 all_narr = emr.get_clin_narrative(episodes = selected_episode_pks, soap_cats = soap_cats)
468
469 if len(all_narr) == 0:
470 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episodes.'))
471 continue
472
473 dlg = cNarrativeListSelectorDlg (
474 parent = parent,
475 id = -1,
476 narrative = all_narr,
477 msg = _(
478 '\n This is the narrative (type %s) for the chosen episodes.\n\n'
479 ' Now, mark the entries you want to include in your report.\n'
480 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soap')) ])
481 )
482 selection_idxs = []
483 for idx in range(len(all_narr)):
484 if all_narr[idx]['pk_narrative'] in selected_narrative_pks:
485 selection_idxs.append(idx)
486 if len(selection_idxs) != 0:
487 dlg.set_selections(selections = selection_idxs)
488 btn_pressed = dlg.ShowModal()
489 selected_narr = dlg.get_selected_item_data()
490 dlg.Destroy()
491
492 if btn_pressed == wx.ID_CANCEL:
493 continue
494
495 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ]
496 for narr in selected_narr:
497 selected_soap[narr['pk_narrative']] = narr
498
500
502
503 narrative = kwargs['narrative']
504 del kwargs['narrative']
505
506 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs)
507
508 self.SetTitle(_('Select the narrative you are interested in ...'))
509
510 self._LCTRL_items.set_columns([_('when'), _('who'), _('type'), _('entry')])
511
512 self._LCTRL_items.set_string_items (
513 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 ]
514 )
515 self._LCTRL_items.set_column_widths()
516 self._LCTRL_items.set_data(data = narrative)
517
518 from Gnumed.wxGladeWidgets import wxgMoveNarrativeDlg
519
521
523
524 self.encounter = kwargs['encounter']
525 self.source_episode = kwargs['episode']
526 del kwargs['encounter']
527 del kwargs['episode']
528
529 wxgMoveNarrativeDlg.wxgMoveNarrativeDlg.__init__(self, *args, **kwargs)
530
531 self.LBL_source_episode.SetLabel(u'%s%s' % (self.source_episode['description'], gmTools.coalesce(self.source_episode['health_issue'], u'', u' (%s)')))
532 self.LBL_encounter.SetLabel('%s: %s %s - %s' % (
533 self.encounter['started'].strftime('%x').decode(gmI18N.get_encoding()),
534 self.encounter['l10n_type'],
535 self.encounter['started'].strftime('%H:%M'),
536 self.encounter['last_affirmed'].strftime('%H:%M')
537 ))
538 pat = gmPerson.gmCurrentPatient()
539 emr = pat.get_emr()
540 narr = emr.get_clin_narrative(episodes=[self.source_episode['pk_episode']], encounters=[self.encounter['pk_encounter']])
541 if len(narr) == 0:
542 narr = [{'narrative': _('There is no narrative for this episode in this encounter.')}]
543 self.LBL_narrative.SetLabel(u'\n'.join([n['narrative'] for n in narr]))
544
545
567
568 from Gnumed.wxGladeWidgets import wxgSoapPluginPnl
569
570 -class cSoapPluginPnl(wxgSoapPluginPnl.wxgSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
571 """A panel for in-context editing of progress notes.
572
573 Expects to be used as a notebook page.
574
575 Left hand side:
576 - problem list (health issues and active episodes)
577 - hints area
578
579 Right hand side:
580 - previous notes
581 - notebook with progress note editors
582 - encounter details fields
583 - visual soap area
584
585 Listens to patient change signals, thus acts on the current patient.
586 """
597
598
599
601
602 if not self.__encounter_valid_for_save():
603 return False
604
605 emr = self.__pat.get_emr()
606 enc = emr.active_encounter
607
608 enc['pk_type'] = self._PRW_encounter_type.GetData()
609 enc['started'] = self._PRW_encounter_start.GetData().get_pydt()
610 enc['last_affirmed'] = self._PRW_encounter_end.GetData().get_pydt()
611 rfe = self._TCTRL_rfe.GetValue().strip()
612 if len(rfe) == 0:
613 enc['reason_for_encounter'] = None
614 else:
615 enc['reason_for_encounter'] = rfe
616 aoe = self._TCTRL_aoe.GetValue().strip()
617 if len(aoe) == 0:
618 enc['assessment_of_encounter'] = None
619 else:
620 enc['assessment_of_encounter'] = aoe
621
622 enc.save_payload()
623
624 return True
625
626
627
629 self._LCTRL_active_problems.set_columns([_('Last'), _('Problem'), _('Health issue')])
630 self._LCTRL_active_problems.set_string_items()
631
632 self._splitter_main.SetSashGravity(0.5)
633 self._splitter_left.SetSashGravity(0.5)
634 self._splitter_right.SetSashGravity(1.0)
635 self._splitter_soap.SetSashGravity(0.75)
636
637 splitter_size = self._splitter_main.GetSizeTuple()[0]
638 self._splitter_main.SetSashPosition(splitter_size * 3 / 10, True)
639
640 splitter_size = self._splitter_left.GetSizeTuple()[1]
641 self._splitter_left.SetSashPosition(splitter_size * 6 / 20, True)
642
643 splitter_size = self._splitter_right.GetSizeTuple()[1]
644 self._splitter_right.SetSashPosition(splitter_size * 15 / 20, True)
645
646 splitter_size = self._splitter_soap.GetSizeTuple()[0]
647 self._splitter_soap.SetSashPosition(splitter_size * 3 / 4, True)
648
649 self._NB_soap_editors.DeleteAllPages()
650
652 """
653 Clear all information from input panel
654 """
655 self._LCTRL_active_problems.set_string_items()
656
657 self._TCTRL_recent_notes.SetValue(u'')
658
659 self._PRW_encounter_type.SetText(suppress_smarts = True)
660 self._PRW_encounter_start.SetText(suppress_smarts = True)
661 self._PRW_encounter_end.SetText(suppress_smarts = True)
662 self._TCTRL_rfe.SetValue(u'')
663 self._TCTRL_aoe.SetValue(u'')
664
665 self._NB_soap_editors.DeleteAllPages()
666 self._NB_soap_editors.add_editor()
667
668 self._PNL_visual_soap.clear()
669
670 self._lbl_hints.SetLabel(u'')
671
673 self._PNL_visual_soap.refresh()
674
676 """Update health problems list.
677 """
678
679 self._LCTRL_active_problems.set_string_items()
680
681 emr = self.__pat.get_emr()
682 problems = emr.get_problems (
683 include_closed_episodes = self._CHBOX_show_closed_episodes.IsChecked(),
684 include_irrelevant_issues = self._CHBOX_irrelevant_issues.IsChecked()
685 )
686
687 list_items = []
688 active_problems = []
689 for problem in problems:
690 if not problem['problem_active']:
691 if not problem['is_potential_problem']:
692 continue
693
694 active_problems.append(problem)
695
696 if problem['type'] == 'issue':
697 issue = emr.problem2issue(problem)
698 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue'])
699 if last_encounter is None:
700 last = issue['modified_when'].strftime('%m/%Y')
701 else:
702 last = last_encounter['last_affirmed'].strftime('%m/%Y')
703
704 list_items.append([last, problem['problem'], gmTools.u_left_arrow])
705
706 elif problem['type'] == 'episode':
707 epi = emr.problem2episode(problem)
708 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode'])
709 if last_encounter is None:
710 last = epi['episode_modified_when'].strftime('%m/%Y')
711 else:
712 last = last_encounter['last_affirmed'].strftime('%m/%Y')
713
714 list_items.append ([
715 last,
716 problem['problem'],
717 gmTools.coalesce(initial = epi['health_issue'], instead = gmTools.u_diameter)
718 ])
719
720 self._LCTRL_active_problems.set_string_items(items = list_items)
721 self._LCTRL_active_problems.set_column_widths()
722 self._LCTRL_active_problems.set_data(data = active_problems)
723
724 showing_potential_problems = (
725 self._CHBOX_show_closed_episodes.IsChecked()
726 or
727 self._CHBOX_irrelevant_issues.IsChecked()
728 )
729 if showing_potential_problems:
730 self._SZR_problem_list_staticbox.SetLabel(_('%s (active+potential) problems') % len(list_items))
731 else:
732 self._SZR_problem_list_staticbox.SetLabel(_('%s active problems') % len(list_items))
733
734 return True
735
737 """This refreshes the recent-notes part."""
738
739 if problem is None:
740 soap = u''
741 caption = u'<?>'
742
743 elif problem['type'] == u'issue':
744 emr = self.__pat.get_emr()
745 soap = u''
746 caption = problem['problem'][:35]
747
748 prev_enc = emr.get_last_but_one_encounter(issue_id = problem['pk_health_issue'])
749 if prev_enc is not None:
750 soap += prev_enc.format (
751 with_soap = True,
752 with_docs = False,
753 with_tests = False,
754 patient = self.__pat,
755 issues = [ problem['pk_health_issue'] ],
756 fancy_header = False
757 )
758
759 tmp = emr.active_encounter.format_soap (
760 soap_cats = 'soap',
761 emr = emr,
762 issues = [ problem['pk_health_issue'] ],
763 )
764 if len(tmp) > 0:
765 soap += _('Current encounter:') + u'\n'
766 soap += u'\n'.join(tmp) + u'\n'
767
768 elif problem['type'] == u'episode':
769 emr = self.__pat.get_emr()
770 soap = u''
771 caption = problem['problem'][:35]
772
773 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_episode'])
774 if prev_enc is None:
775 if problem['pk_health_issue'] is not None:
776 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_health_issue'])
777 if prev_enc is not None:
778 soap += prev_enc.format (
779 with_soap = True,
780 with_docs = False,
781 with_tests = False,
782 patient = self.__pat,
783 issues = [ problem['pk_health_issue'] ],
784 fancy_header = False
785 )
786 else:
787 soap += prev_enc.format (
788 episodes = [ problem['pk_episode'] ],
789 with_soap = True,
790 with_docs = False,
791 with_tests = False,
792 patient = self.__pat,
793 fancy_header = False
794 )
795
796 tmp = emr.active_encounter.format_soap (
797 soap_cats = 'soap',
798 emr = emr,
799 issues = [ problem['pk_health_issue'] ],
800 )
801 if len(tmp) > 0:
802 soap += _('Current encounter:') + u'\n'
803 soap += u'\n'.join(tmp) + u'\n'
804
805 else:
806 soap = u''
807 caption = u'<?>'
808
809 self._TCTRL_recent_notes.SetValue(soap)
810 self._TCTRL_recent_notes.ShowPosition(self._TCTRL_recent_notes.GetLastPosition())
811 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on %s%s%s') % (
812 gmTools.u_left_double_angle_quote,
813 caption,
814 gmTools.u_right_double_angle_quote
815 ))
816
817 self._TCTRL_recent_notes.Refresh()
818
819 return True
820
848
850 """Assumes that the field data is valid."""
851
852 emr = self.__pat.get_emr()
853 enc = emr.active_encounter
854
855 data = {
856 'pk_type': self._PRW_encounter_type.GetData(),
857 'reason_for_encounter': gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u''),
858 'assessment_of_encounter': gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
859 'pk_location': enc['pk_location'],
860 'pk_patient': enc['pk_patient']
861 }
862
863 if self._PRW_encounter_start.GetData() is None:
864 data['started'] = None
865 else:
866 data['started'] = self._PRW_encounter_start.GetData().get_pydt()
867
868 if self._PRW_encounter_end.GetData() is None:
869 data['last_affirmed'] = None
870 else:
871 data['last_affirmed'] = self._PRW_encounter_end.GetData().get_pydt()
872
873 return not enc.same_payload(another_object = data)
874
876
877 found_error = False
878
879 if self._PRW_encounter_type.GetData() is None:
880 found_error = True
881 msg = _('Cannot save encounter: missing type.')
882
883 if self._PRW_encounter_start.GetData() is None:
884 found_error = True
885 msg = _('Cannot save encounter: missing start time.')
886
887 if self._PRW_encounter_end.GetData() is None:
888 found_error = True
889 msg = _('Cannot save encounter: missing end time.')
890
891 if found_error:
892 gmDispatcher.send(signal = 'statustext', msg = msg, beep = True)
893 return False
894
895 return True
896
897
898
900 """Configure enabled event signals."""
901
902 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
903 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
904 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db)
905 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
906 gmDispatcher.connect(signal = u'doc_mod_db', receiver = self._on_doc_mod_db)
907 gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified)
908 gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_switched)
909
910
911 self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback)
912 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
913
915 """Another patient is about to be activated.
916
917 Patient change will not proceed before this returns True.
918 """
919
920
921 if not self.__pat.connected:
922 return True
923 return self._NB_soap_editors.warn_on_unsaved_soap()
924
926 """The client is about to be shut down.
927
928 Shutdown will not proceed before this returns.
929 """
930 if not self.__pat.connected:
931 return True
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947 emr = self.__pat.get_emr()
948 saved = self._NB_soap_editors.save_all_editors (
949 emr = emr,
950 episode_name_candidates = [
951 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
952 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'')
953 ]
954 )
955 if not saved:
956 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
957 return True
958
960 wx.CallAfter(self.__on_pre_patient_selection)
961
963 self.__reset_ui_content()
964
966 wx.CallAfter(self._schedule_data_reget)
967
969 wx.CallAfter(self.__refresh_visual_soaps)
970
972 wx.CallAfter(self._schedule_data_reget)
973
975 wx.CallAfter(self.__refresh_encounter)
976
978 wx.CallAfter(self.__on_current_encounter_switched)
979
981 self.__refresh_encounter()
982 self.__refresh_visual_soaps()
983
985 """Show related note at the bottom."""
986 pass
987
989 """Show related note at the bottom."""
990 emr = self.__pat.get_emr()
991 self.__refresh_recent_notes (
992 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
993 )
994
996 """Open progress note editor for this problem.
997 """
998 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
999 if problem is None:
1000 return True
1001
1002 dbcfg = gmCfg.cCfgSQL()
1003 allow_duplicate_editors = bool(dbcfg.get2 (
1004 option = u'horstspace.soap_editor.allow_same_episode_multiple_times',
1005 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1006 bias = u'user',
1007 default = False
1008 ))
1009 if self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors):
1010 return True
1011
1012 gmGuiHelpers.gm_show_error (
1013 aMessage = _(
1014 'Cannot open progress note editor for\n\n'
1015 '[%s].\n\n'
1016 ) % problem['problem'],
1017 aTitle = _('opening progress note editor')
1018 )
1019 event.Skip()
1020 return False
1021
1025
1029
1033
1042
1054
1058
1069
1093
1095 self.__refresh_problem_list()
1096
1098 self.__refresh_problem_list()
1099
1100
1101
1103 self.__refresh_problem_list()
1104 self.__refresh_encounter()
1105 self.__refresh_visual_soaps()
1106 return True
1107
1281
1282 from Gnumed.wxGladeWidgets import wxgSoapNoteExpandoEditAreaPnl
1283
1285
1287
1288 try:
1289 self.problem = kwargs['problem']
1290 del kwargs['problem']
1291 except KeyError:
1292 self.problem = None
1293
1294 wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl.__init__(self, *args, **kwargs)
1295
1296 self.fields = [
1297 self._TCTRL_Soap,
1298 self._TCTRL_sOap,
1299 self._TCTRL_soAp,
1300 self._TCTRL_soaP
1301 ]
1302
1303 self.__register_interests()
1304
1306 for field in self.fields:
1307 field.SetValue(u'')
1308
1309 - def save(self, emr=None, episode_name_candidates=None):
1310
1311 if self.empty:
1312 return True
1313
1314
1315 if (self.problem is None) or (self.problem['type'] == 'issue'):
1316
1317 episode_name_candidates.append(u'')
1318 for candidate in episode_name_candidates:
1319 if candidate is None:
1320 continue
1321 epi_name = candidate.strip().replace('\r', '//').replace('\n', '//')
1322 break
1323
1324 dlg = wx.TextEntryDialog (
1325 parent = self,
1326 message = _('Enter a short working name for this new problem:'),
1327 caption = _('Creating a problem (episode) to save the notelet under ...'),
1328 defaultValue = epi_name,
1329 style = wx.OK | wx.CANCEL | wx.CENTRE
1330 )
1331 decision = dlg.ShowModal()
1332 if decision != wx.ID_OK:
1333 return False
1334
1335 epi_name = dlg.GetValue().strip()
1336 if epi_name == u'':
1337 gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note'))
1338 return False
1339
1340
1341 new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True)
1342
1343 if self.problem is not None:
1344 issue = emr.problem2issue(self.problem)
1345 if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True):
1346 gmGuiHelpers.gm_show_warning (
1347 _(
1348 'The new episode:\n'
1349 '\n'
1350 ' "%s"\n'
1351 '\n'
1352 'will remain unassociated despite the editor\n'
1353 'having been invoked from the health issue:\n'
1354 '\n'
1355 ' "%s"'
1356 ) % (
1357 new_episode['description'],
1358 issue['description']
1359 ),
1360 _('saving progress note')
1361 )
1362
1363 epi_id = new_episode['pk_episode']
1364 else:
1365 epi_id = self.problem['pk_episode']
1366
1367 emr.add_notes(notes = self.soap, episode = epi_id)
1368
1369 return True
1370
1371
1372
1374 for field in self.fields:
1375 wxexpando.EVT_ETC_LAYOUT_NEEDED(field, field.GetId(), self._on_expando_needs_layout)
1376
1378
1379
1380
1381 self.Fit()
1382
1383 if self.HasScrollbar(wx.VERTICAL):
1384
1385 expando = self.FindWindowById(evt.GetId())
1386 y_expando = expando.GetPositionTuple()[1]
1387 h_expando = expando.GetSizeTuple()[1]
1388 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1
1389 y_cursor = int(round((float(line_cursor) / expando.NumberOfLines) * h_expando))
1390 y_desired_visible = y_expando + y_cursor
1391
1392 y_view = self.ViewStart[1]
1393 h_view = self.GetClientSizeTuple()[1]
1394
1395
1396
1397
1398
1399
1400
1401
1402 if y_desired_visible < y_view:
1403
1404 self.Scroll(0, y_desired_visible)
1405
1406 if y_desired_visible > h_view:
1407
1408 self.Scroll(0, y_desired_visible)
1409
1410
1411
1413 note = []
1414
1415 tmp = self._TCTRL_Soap.GetValue().strip()
1416 if tmp != u'':
1417 note.append(['s', tmp])
1418
1419 tmp = self._TCTRL_sOap.GetValue().strip()
1420 if tmp != u'':
1421 note.append(['o', tmp])
1422
1423 tmp = self._TCTRL_soAp.GetValue().strip()
1424 if tmp != u'':
1425 note.append(['a', tmp])
1426
1427 tmp = self._TCTRL_soaP.GetValue().strip()
1428 if tmp != u'':
1429 note.append(['p', tmp])
1430
1431 return note
1432
1433 soap = property(_get_soap, lambda x:x)
1434
1436 for field in self.fields:
1437 if field.GetValue().strip() != u'':
1438 return False
1439 return True
1440
1441 empty = property(_get_empty, lambda x:x)
1442
1443 -class cSoapLineTextCtrl(wxexpando.ExpandoTextCtrl):
1444
1445 - def __init__(self, *args, **kwargs):
1446
1447 wxexpando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
1448
1449 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+")
1450
1451 self.__register_interests()
1452
1453
1454
1456
1457
1458 wx.EVT_CHAR(self, self.__on_char)
1459 wx.EVT_SET_FOCUS(self, self.__on_focus)
1460
1461 - def __on_focus(self, evt):
1462 evt.Skip()
1463 wx.CallAfter(self._after_on_focus)
1464
1465 - def _after_on_focus(self):
1466 evt = wx.PyCommandEvent(wxexpando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
1467 evt.SetEventObject(self)
1468 evt.height = None
1469 evt.numLines = None
1470 self.GetEventHandler().ProcessEvent(evt)
1471
1472 - def __on_char(self, evt):
1473 char = unichr(evt.GetUnicodeKey())
1474
1475 if self.LastPosition == 1:
1476 evt.Skip()
1477 return
1478
1479 explicit_expansion = False
1480 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT):
1481 if evt.GetKeyCode() != 13:
1482 evt.Skip()
1483 return
1484 explicit_expansion = True
1485
1486 if not explicit_expansion:
1487 if self.__keyword_separators.match(char) is None:
1488 evt.Skip()
1489 return
1490
1491 caret_pos, line_no = self.PositionToXY(self.InsertionPoint)
1492 line = self.GetLineText(line_no)
1493 word = self.__keyword_separators.split(line[:caret_pos])[-1]
1494
1495 if (
1496 (not explicit_expansion)
1497 and
1498 (word != u'$$steffi')
1499 and
1500 (word not in [ r[0] for r in gmPG2.get_text_expansion_keywords() ])
1501 ):
1502 evt.Skip()
1503 return
1504
1505 start = self.InsertionPoint - len(word)
1506 wx.CallAfter(self.replace_keyword_with_expansion, word, start, explicit_expansion)
1507
1508 evt.Skip()
1509 return
1510
1511 - def replace_keyword_with_expansion(self, keyword=None, position=None, show_list=False):
1512
1513 if show_list:
1514 candidates = gmPG2.get_keyword_expansion_candidates(keyword = keyword)
1515 if len(candidates) == 0:
1516 return
1517 if len(candidates) == 1:
1518 keyword = candidates[0]
1519 else:
1520 keyword = gmListWidgets.get_choices_from_list (
1521 parent = self,
1522 msg = _(
1523 'Several macros match the keyword [%s].\n'
1524 '\n'
1525 'Please select the expansion you want to happen.'
1526 ) % keyword,
1527 caption = _('Selecting text macro'),
1528 choices = candidates,
1529 columns = [_('Keyword')],
1530 single_selection = True,
1531 can_return_empty = False
1532 )
1533 if keyword is None:
1534 return
1535
1536 expansion = gmPG2.expand_keyword(keyword = keyword)
1537
1538 if expansion is None:
1539 return
1540
1541 if expansion == u'':
1542 return
1543
1544 self.Replace (
1545 position,
1546 position + len(keyword),
1547 expansion
1548 )
1549
1550 self.SetInsertionPoint(position + len(expansion) + 1)
1551 self.ShowPosition(position + len(expansion) + 1)
1552
1553 return
1554
1555
1556
1557 visual_progress_note_document_type = u'visual progress note'
1558
1559
1590
1591 gmCfgWidgets.configure_string_option (
1592 message = _(
1593 'Enter the shell command with which to start\n'
1594 'the image editor for visual progress notes.\n'
1595 '\n'
1596 'Any "%(img)s" included with the arguments\n'
1597 'will be replaced by the file name of the\n'
1598 'note template.'
1599 ),
1600 option = u'external.tools.visual_soap_editor_cmd',
1601 bias = 'user',
1602 default_value = None,
1603 validator = is_valid
1604 )
1605
1607 """This assumes <filename> contains an image which can be handled by the configured image editor."""
1608
1609 if doc_part is not None:
1610 filename = doc_part.export_to_file()
1611 if filename is None:
1612 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.'))
1613 return None
1614
1615 dbcfg = gmCfg.cCfgSQL()
1616 cmd = dbcfg.get2 (
1617 option = u'external.tools.visual_soap_editor_cmd',
1618 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1619 bias = 'user'
1620 )
1621
1622 if cmd is None:
1623 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = False)
1624 cmd = configure_visual_progress_note_editor()
1625 if cmd is None:
1626 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = True)
1627 return None
1628
1629 if u'%(img)s' in cmd:
1630 cmd % {u'img': filename}
1631 else:
1632 cmd = u'%s %s' % (cmd, filename)
1633
1634 if discard_unmodified:
1635 original_stat = os.stat(filename)
1636 original_md5 = gmTools.file2md5(filename)
1637
1638 success = gmShellAPI.run_command_in_shell(cmd, blocking = True)
1639 if not success:
1640 gmGuiHelpers.gm_show_error (
1641 _(
1642 'There was a problem with running the editor\n'
1643 'for visual progress notes.\n'
1644 '\n'
1645 ' [%s]\n'
1646 '\n'
1647 ) % cmd,
1648 _('Editing visual progress note')
1649 )
1650 return None
1651
1652 try:
1653 open(filename, 'r').close()
1654 except StandardError:
1655 _log.exception('problem accessing visual progress note file [%s]', filename)
1656 gmGuiHelpers.gm_show_error (
1657 _(
1658 'There was a problem reading the visual\n'
1659 'progress note from the file:\n'
1660 '\n'
1661 ' [%s]\n'
1662 '\n'
1663 ) % filename,
1664 _('Saving visual progress note')
1665 )
1666 return None
1667
1668 if discard_unmodified:
1669 modified_stat = os.stat(filename)
1670
1671 if original_stat.st_size == modified_stat.st_size:
1672 modified_md5 = gmTools.file2md5(filename)
1673
1674 if original_md5 == modified_md5:
1675 _log.debug('visual progress note (template) not modified')
1676
1677 msg = _(
1678 u'This visual progress note was created from a\n'
1679 u'template in the database rather than from a file\n'
1680 u'but the image was not modified at all.\n'
1681 u'\n'
1682 u'Do you want to still save the unmodified\n'
1683 u'image as a visual progress note into the\n'
1684 u'EMR of the patient ?'
1685 )
1686 save_unmodified = gmGuiHelpers.gm_show_question (
1687 msg,
1688 _('Saving visual progress note')
1689 )
1690 if not save_unmodified:
1691 _log.debug('user discarded unmodified note')
1692 return
1693
1694 if doc_part is not None:
1695 doc_part.update_data_from_file(fname = filename)
1696 doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True)
1697 return None
1698
1699 if not isinstance(episode, gmEMRStructItems.cEpisode):
1700 pat = gmPerson.gmCurrentPatient()
1701 emr = pat.get_emr()
1702 episode = emr.add_episode(episode_name = episode.strip(), is_open = False)
1703
1704 doc = gmDocumentWidgets.save_file_as_new_document (
1705 filename = filename,
1706 document_type = visual_progress_note_document_type,
1707 episode = episode,
1708 unlock_patient = True
1709 )
1710 doc.set_reviewed(technically_abnormal = False, clinically_relevant = True)
1711
1712 return doc
1713
1715 """Phrasewheel to allow selection of visual SOAP template."""
1716
1718
1719 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
1720
1721 query = u"""
1722 SELECT
1723 pk,
1724 name_short
1725 FROM
1726 ref.paperwork_templates
1727 WHERE
1728 fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND (
1729 name_long %%(fragment_condition)s
1730 OR
1731 name_short %%(fragment_condition)s
1732 )
1733 ORDER BY name_short
1734 LIMIT 15
1735 """ % visual_progress_note_document_type
1736
1737 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1738 mp.setThresholds(2, 3, 5)
1739
1740 self.matcher = mp
1741 self.selection_only = True
1742
1748
1749 from Gnumed.wxGladeWidgets import wxgVisualSoapPnl
1750
1752
1759
1760
1761
1768
1769 - def refresh(self, patient=None, encounter=None):
1803
1844
1845
1846
1855
1857 self._BTN_delete.Enable(False)
1858
1875
1905
1968
1986
1987
1988
1989 if __name__ == '__main__':
1990
1991 if len(sys.argv) < 2:
1992 sys.exit()
1993
1994 if sys.argv[1] != 'test':
1995 sys.exit()
1996
1997 gmI18N.activate_locale()
1998 gmI18N.install_domain(domain = 'gnumed')
1999
2000
2009
2016
2029
2030
2031 test_cSoapNoteExpandoEditAreaPnl()
2032
2033
2034
2035