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 if not self._NB_soap_editors.save_all_editors(emr = emr, rfe = self._TCTRL_rfe.GetValue().strip(), aoe = self._TCTRL_aoe.GetValue().strip()):
949 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
950 return True
951
953 wx.CallAfter(self.__on_pre_patient_selection)
954
956 self.__reset_ui_content()
957
959 wx.CallAfter(self._schedule_data_reget)
960
962 wx.CallAfter(self.__refresh_visual_soaps)
963
965 wx.CallAfter(self._schedule_data_reget)
966
968 wx.CallAfter(self.__refresh_encounter)
969
971 wx.CallAfter(self.__on_current_encounter_switched)
972
974 self.__refresh_encounter()
975 self.__refresh_visual_soaps()
976
978 """Show related note at the bottom."""
979 pass
980
982 """Show related note at the bottom."""
983 emr = self.__pat.get_emr()
984 self.__refresh_recent_notes (
985 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
986 )
987
989 """Open progress note editor for this problem.
990 """
991 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
992 if problem is None:
993 return True
994
995 dbcfg = gmCfg.cCfgSQL()
996 allow_duplicate_editors = bool(dbcfg.get2 (
997 option = u'horstspace.soap_editor.allow_same_episode_multiple_times',
998 workplace = gmSurgery.gmCurrentPractice().active_workplace,
999 bias = u'user',
1000 default = False
1001 ))
1002 if self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors):
1003 return True
1004
1005 gmGuiHelpers.gm_show_error (
1006 aMessage = _(
1007 'Cannot open progress note editor for\n\n'
1008 '[%s].\n\n'
1009 ) % problem['problem'],
1010 aTitle = _('opening progress note editor')
1011 )
1012 event.Skip()
1013 return False
1014
1018
1022
1026
1033
1037
1046
1070
1072 self.__refresh_problem_list()
1073
1075 self.__refresh_problem_list()
1076
1077
1078
1080 self.__refresh_problem_list()
1081 self.__refresh_encounter()
1082 self.__refresh_visual_soaps()
1083 return True
1084
1248
1249 from Gnumed.wxGladeWidgets import wxgSoapNoteExpandoEditAreaPnl
1250
1252
1254
1255 try:
1256 self.problem = kwargs['problem']
1257 del kwargs['problem']
1258 except KeyError:
1259 self.problem = None
1260
1261 wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl.__init__(self, *args, **kwargs)
1262
1263 self.fields = [
1264 self._TCTRL_Soap,
1265 self._TCTRL_sOap,
1266 self._TCTRL_soAp,
1267 self._TCTRL_soaP
1268 ]
1269
1270 self.__register_interests()
1271
1273 for field in self.fields:
1274 field.SetValue(u'')
1275
1276 - def save(self, emr=None, rfe=None, aoe=None):
1277
1278 if self.empty:
1279 return True
1280
1281
1282 if (self.problem is None) or (self.problem['type'] == 'issue'):
1283
1284 epi_name = gmTools.coalesce (
1285 aoe,
1286 gmTools.coalesce (
1287 rfe,
1288 u''
1289 )
1290 ).strip().replace('\r', '//').replace('\n', '//')
1291
1292 dlg = wx.TextEntryDialog (
1293 parent = self,
1294 message = _('Enter a short working name for this new problem:'),
1295 caption = _('Creating a problem (episode) to save the notelet under ...'),
1296 defaultValue = epi_name,
1297 style = wx.OK | wx.CANCEL | wx.CENTRE
1298 )
1299 decision = dlg.ShowModal()
1300 if decision != wx.ID_OK:
1301 return False
1302
1303 epi_name = dlg.GetValue().strip()
1304 if epi_name == u'':
1305 gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note'))
1306 return False
1307
1308
1309 new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True)
1310
1311 if self.problem is not None:
1312 issue = emr.problem2issue(self.problem)
1313 if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True):
1314 gmGuiHelpers.gm_show_warning (
1315 _(
1316 'The new episode:\n'
1317 '\n'
1318 ' "%s"\n'
1319 '\n'
1320 'will remain unassociated despite the editor\n'
1321 'having been invoked from the health issue:\n'
1322 '\n'
1323 ' "%s"'
1324 ) % (
1325 new_episode['description'],
1326 issue['description']
1327 ),
1328 _('saving progress note')
1329 )
1330
1331 epi_id = new_episode['pk_episode']
1332 else:
1333 epi_id = self.problem['pk_episode']
1334
1335 emr.add_notes(notes = self.soap, episode = epi_id)
1336
1337 return True
1338
1339
1340
1342 for field in self.fields:
1343 wxexpando.EVT_ETC_LAYOUT_NEEDED(field, field.GetId(), self._on_expando_needs_layout)
1344
1346
1347
1348
1349 self.Fit()
1350
1351 if self.HasScrollbar(wx.VERTICAL):
1352
1353 expando = self.FindWindowById(evt.GetId())
1354 y_expando = expando.GetPositionTuple()[1]
1355 h_expando = expando.GetSizeTuple()[1]
1356 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1
1357 y_cursor = int(round((float(line_cursor) / expando.NumberOfLines) * h_expando))
1358 y_desired_visible = y_expando + y_cursor
1359
1360 y_view = self.ViewStart[1]
1361 h_view = self.GetClientSizeTuple()[1]
1362
1363
1364
1365
1366
1367
1368
1369
1370 if y_desired_visible < y_view:
1371
1372 self.Scroll(0, y_desired_visible)
1373
1374 if y_desired_visible > h_view:
1375
1376 self.Scroll(0, y_desired_visible)
1377
1378
1379
1381 note = []
1382
1383 tmp = self._TCTRL_Soap.GetValue().strip()
1384 if tmp != u'':
1385 note.append(['s', tmp])
1386
1387 tmp = self._TCTRL_sOap.GetValue().strip()
1388 if tmp != u'':
1389 note.append(['o', tmp])
1390
1391 tmp = self._TCTRL_soAp.GetValue().strip()
1392 if tmp != u'':
1393 note.append(['a', tmp])
1394
1395 tmp = self._TCTRL_soaP.GetValue().strip()
1396 if tmp != u'':
1397 note.append(['p', tmp])
1398
1399 return note
1400
1401 soap = property(_get_soap, lambda x:x)
1402
1404 for field in self.fields:
1405 if field.GetValue().strip() != u'':
1406 return False
1407 return True
1408
1409 empty = property(_get_empty, lambda x:x)
1410
1411 -class cSoapLineTextCtrl(wxexpando.ExpandoTextCtrl):
1412
1413 - def __init__(self, *args, **kwargs):
1414
1415 wxexpando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
1416
1417 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+")
1418
1419 self.__register_interests()
1420
1421
1422
1424
1425
1426 wx.EVT_CHAR(self, self.__on_char)
1427 wx.EVT_SET_FOCUS(self, self.__on_focus)
1428
1429 - def __on_focus(self, evt):
1430 evt.Skip()
1431 wx.CallAfter(self._after_on_focus)
1432
1433 - def _after_on_focus(self):
1434 evt = wx.PyCommandEvent(wxexpando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
1435 evt.SetEventObject(self)
1436 evt.height = None
1437 evt.numLines = None
1438 self.GetEventHandler().ProcessEvent(evt)
1439
1440 - def __on_char(self, evt):
1441 char = unichr(evt.GetUnicodeKey())
1442
1443 if self.LastPosition == 1:
1444 evt.Skip()
1445 return
1446
1447 explicit_expansion = False
1448 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT):
1449 if evt.GetKeyCode() != 13:
1450 evt.Skip()
1451 return
1452 explicit_expansion = True
1453
1454 if not explicit_expansion:
1455 if self.__keyword_separators.match(char) is None:
1456 evt.Skip()
1457 return
1458
1459 caret_pos, line_no = self.PositionToXY(self.InsertionPoint)
1460 line = self.GetLineText(line_no)
1461 word = self.__keyword_separators.split(line[:caret_pos])[-1]
1462
1463 if (
1464 (not explicit_expansion)
1465 and
1466 (word != u'$$steffi')
1467 and
1468 (word not in [ r[0] for r in gmPG2.get_text_expansion_keywords() ])
1469 ):
1470 evt.Skip()
1471 return
1472
1473 start = self.InsertionPoint - len(word)
1474 wx.CallAfter(self.replace_keyword_with_expansion, word, start, explicit_expansion)
1475
1476 evt.Skip()
1477 return
1478
1479 - def replace_keyword_with_expansion(self, keyword=None, position=None, show_list=False):
1480
1481 if show_list:
1482 candidates = gmPG2.get_keyword_expansion_candidates(keyword = keyword)
1483 if len(candidates) == 0:
1484 return
1485 if len(candidates) == 1:
1486 keyword = candidates[0]
1487 else:
1488 keyword = gmListWidgets.get_choices_from_list (
1489 parent = self,
1490 msg = _(
1491 'Several macros match the keyword [%s].\n'
1492 '\n'
1493 'Please select the expansion you want to happen.'
1494 ) % keyword,
1495 caption = _('Selecting text macro'),
1496 choices = candidates,
1497 columns = [_('Keyword')],
1498 single_selection = True,
1499 can_return_empty = False
1500 )
1501 if keyword is None:
1502 return
1503
1504 expansion = gmPG2.expand_keyword(keyword = keyword)
1505
1506 if expansion is None:
1507 return
1508
1509 if expansion == u'':
1510 return
1511
1512 self.Replace (
1513 position,
1514 position + len(keyword),
1515 expansion
1516 )
1517
1518 self.SetInsertionPoint(position + len(expansion) + 1)
1519 self.ShowPosition(position + len(expansion) + 1)
1520
1521 return
1522
1523
1524
1525 visual_progress_note_document_type = u'visual progress note'
1526
1527
1558
1559 gmCfgWidgets.configure_string_option (
1560 message = _(
1561 'Enter the shell command with which to start\n'
1562 'the image editor for visual progress notes.\n'
1563 '\n'
1564 'Any "%(img)s" included with the arguments\n'
1565 'will be replaced by the file name of the\n'
1566 'note template.'
1567 ),
1568 option = u'external.tools.visual_soap_editor_cmd',
1569 bias = 'user',
1570 default_value = None,
1571 validator = is_valid
1572 )
1573
1575 """This assumes <filename> contains an image which can be handled by the configured image editor."""
1576
1577 if doc_part is not None:
1578 filename = doc_part.export_to_file()
1579 if filename is None:
1580 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.'))
1581 return None
1582
1583 dbcfg = gmCfg.cCfgSQL()
1584 cmd = dbcfg.get2 (
1585 option = u'external.tools.visual_soap_editor_cmd',
1586 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1587 bias = 'user'
1588 )
1589
1590 if cmd is None:
1591 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = False)
1592 cmd = configure_visual_progress_note_editor()
1593 if cmd is None:
1594 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = True)
1595 return None
1596
1597 if u'%(img)s' in cmd:
1598 cmd % {u'img': filename}
1599 else:
1600 cmd = u'%s %s' % (cmd, filename)
1601
1602 if discard_unmodified:
1603 original_stat = os.stat(filename)
1604 original_md5 = gmTools.file2md5(filename)
1605
1606 success = gmShellAPI.run_command_in_shell(cmd, blocking = True)
1607 if not success:
1608 gmGuiHelpers.gm_show_error (
1609 _(
1610 'There was a problem with running the editor\n'
1611 'for visual progress notes.\n'
1612 '\n'
1613 ' [%s]\n'
1614 '\n'
1615 ) % cmd,
1616 _('Editing visual progress note')
1617 )
1618 return None
1619
1620 try:
1621 open(filename, 'r').close()
1622 except StandardError:
1623 _log.exception('problem accessing visual progress note file [%s]', filename)
1624 gmGuiHelpers.gm_show_error (
1625 _(
1626 'There was a problem reading the visual\n'
1627 'progress note from the file:\n'
1628 '\n'
1629 ' [%s]\n'
1630 '\n'
1631 ) % filename,
1632 _('Saving visual progress note')
1633 )
1634 return None
1635
1636 if discard_unmodified:
1637 modified_stat = os.stat(filename)
1638
1639 if original_stat.st_size == modified_stat.st_size:
1640 modified_md5 = gmTools.file2md5(filename)
1641
1642 if original_md5 == modified_md5:
1643 _log.debug('visual progress note (template) not modified')
1644
1645 msg = _(
1646 u'This visual progress note was created from a\n'
1647 u'template in the database rather than from a file\n'
1648 u'but the image was not modified at all.\n'
1649 u'\n'
1650 u'Do you want to still save the unmodified\n'
1651 u'image as a visual progress note into the\n'
1652 u'EMR of the patient ?'
1653 )
1654 save_unmodified = gmGuiHelpers.gm_show_question (
1655 msg,
1656 _('Saving visual progress note')
1657 )
1658 if not save_unmodified:
1659 _log.debug('user discarded unmodified note')
1660 return
1661
1662 if doc_part is not None:
1663 doc_part.update_data_from_file(fname = filename)
1664 doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True)
1665 return None
1666
1667 if not isinstance(episode, gmEMRStructItems.cEpisode):
1668 pat = gmPerson.gmCurrentPatient()
1669 emr = pat.get_emr()
1670 episode = emr.add_episode(episode_name = episode.strip(), is_open = False)
1671
1672 doc = gmDocumentWidgets.save_file_as_new_document (
1673 filename = filename,
1674 document_type = visual_progress_note_document_type,
1675 episode = episode,
1676 unlock_patient = True
1677 )
1678 doc.set_reviewed(technically_abnormal = False, clinically_relevant = True)
1679
1680 return doc
1681
1683 """Phrasewheel to allow selection of visual SOAP template."""
1684
1686
1687 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
1688
1689 query = u"""
1690 SELECT
1691 pk,
1692 name_short
1693 FROM
1694 ref.paperwork_templates
1695 WHERE
1696 fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND (
1697 name_long %%(fragment_condition)s
1698 OR
1699 name_short %%(fragment_condition)s
1700 )
1701 ORDER BY name_short
1702 LIMIT 15
1703 """ % visual_progress_note_document_type
1704
1705 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1706 mp.setThresholds(2, 3, 5)
1707
1708 self.matcher = mp
1709 self.selection_only = True
1710
1716
1717 from Gnumed.wxGladeWidgets import wxgVisualSoapPnl
1718
1720
1727
1728
1729
1736
1737 - def refresh(self, patient=None, encounter=None):
1771
1812
1813
1814
1823
1825 self._BTN_delete.Enable(False)
1826
1843
1873
1936
1954
1955
1956
1957 if __name__ == '__main__':
1958
1959 if len(sys.argv) < 2:
1960 sys.exit()
1961
1962 if sys.argv[1] != 'test':
1963 sys.exit()
1964
1965 gmI18N.activate_locale()
1966 gmI18N.install_domain(domain = 'gnumed')
1967
1968
1977
1984
1997
1998
1999 test_cSoapNoteExpandoEditAreaPnl()
2000
2001
2002
2003