1 """GNUmed narrative handling widgets."""
2
3
4
5 __version__ = "$Revision: 1.46 $"
6 __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
7
8 import sys, logging, os, os.path, time, re as regex
9
10
11 import wx
12 import wx.lib.expando as wxexpando
13
14
15 if __name__ == '__main__':
16 sys.path.insert(0, '../../')
17 from Gnumed.pycommon import gmI18N, gmDispatcher, gmTools, gmDateTime, gmPG2, gmCfg
18 from Gnumed.business import gmPerson, gmEMRStructItems, gmClinNarrative, gmSurgery
19 from Gnumed.exporters import gmPatientExporter
20 from Gnumed.wxpython import gmListWidgets, gmEMRStructWidgets, gmRegetMixin, gmGuiHelpers, gmPatSearchWidgets
21 from Gnumed.wxGladeWidgets import wxgMoveNarrativeDlg, wxgSoapNoteExpandoEditAreaPnl
22
23
24 _log = logging.getLogger('gm.ui')
25 _log.info(__version__)
26
27
28
30
31
32 if patient is None:
33 patient = gmPerson.gmCurrentPatient()
34
35 if not patient.connected:
36 gmDispatcher.send(signal = 'statustext', msg = _('Cannot move progress notes. No active patient.'))
37 return False
38
39 if parent is None:
40 parent = wx.GetApp().GetTopWindow()
41
42 emr = patient.get_emr()
43
44 if encounters is None:
45 encs = emr.get_encounters(episodes = episodes)
46 encounters = gmEMRStructWidgets.select_encounters (
47 parent = parent,
48 patient = patient,
49 single_selection = False,
50 encounters = encs
51 )
52
53 notes = emr.get_clin_narrative (
54 encounters = encounters,
55 episodes = episodes
56 )
57
58
59 if move_all:
60 selected_narr = notes
61 else:
62 selected_narr = gmListWidgets.get_choices_from_list (
63 parent = parent,
64 caption = _('Moving progress notes between encounters ...'),
65 single_selection = False,
66 can_return_empty = True,
67 data = notes,
68 msg = _('\n Select the progress notes to move from the list !\n\n'),
69 columns = [_('when'), _('who'), _('type'), _('entry')],
70 choices = [
71 [ narr['date'].strftime('%x %H:%M'),
72 narr['provider'],
73 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
74 narr['narrative'].replace('\n', '/').replace('\r', '/')
75 ] for narr in notes
76 ]
77 )
78
79 if not selected_narr:
80 return True
81
82
83 enc2move2 = gmEMRStructWidgets.select_encounters (
84 parent = parent,
85 patient = patient,
86 single_selection = True
87 )
88
89 if not enc2move2:
90 return True
91
92 for narr in selected_narr:
93 narr['pk_encounter'] = enc2move2['pk_encounter']
94 narr.save()
95
96 return True
97
99
100
101 if patient is None:
102 patient = gmPerson.gmCurrentPatient()
103
104 if not patient.connected:
105 gmDispatcher.send(signal = 'statustext', msg = _('Cannot edit progress notes. No active patient.'))
106 return False
107
108 if parent is None:
109 parent = wx.GetApp().GetTopWindow()
110
111 emr = patient.get_emr()
112
113 def delete(item):
114 if item is None:
115 return False
116 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
117 parent,
118 -1,
119 caption = _('Deleting progress note'),
120 question = _(
121 'Are you positively sure you want to delete this\n'
122 'progress note from the medical record ?\n'
123 '\n'
124 'Note that even if you chose to delete the entry it will\n'
125 'still be (invisibly) kept in the audit trail to protect\n'
126 'you from litigation because physical deletion is known\n'
127 'to be unlawful in some jurisdictions.\n'
128 ),
129 button_defs = (
130 {'label': _('Delete'), 'tooltip': _('Yes, delete the progress note.'), 'default': False},
131 {'label': _('Cancel'), 'tooltip': _('No, do NOT delete the progress note.'), 'default': True}
132 )
133 )
134 decision = dlg.ShowModal()
135
136 if decision != wx.ID_YES:
137 return False
138
139 gmClinNarrative.delete_clin_narrative(narrative = item['pk_narrative'])
140 return True
141
142 def edit(item):
143 if item is None:
144 return False
145
146 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
147 parent,
148 -1,
149 title = _('Editing progress note'),
150 msg = _('This is the original progress note:'),
151 data = item.format(left_margin = u' ', fancy = True),
152 text = item['narrative']
153 )
154 decision = dlg.ShowModal()
155
156 if decision != wx.ID_SAVE:
157 return False
158
159 val = dlg.value
160 dlg.Destroy()
161 if val.strip() == u'':
162 return False
163
164 item['narrative'] = val
165 item.save_payload()
166
167 return True
168
169 def refresh(lctrl):
170 notes = emr.get_clin_narrative (
171 encounters = encounters,
172 episodes = episodes,
173 providers = [ gmPerson.gmCurrentProvider()['short_alias'] ]
174 )
175 lctrl.set_string_items(items = [
176 [ narr['date'].strftime('%x %H:%M'),
177 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
178 narr['narrative'].replace('\n', '/').replace('\r', '/')
179 ] for narr in notes
180 ])
181 lctrl.set_data(data = notes)
182
183
184 gmListWidgets.get_choices_from_list (
185 parent = parent,
186 caption = _('Managing progress notes'),
187 msg = _(
188 '\n'
189 ' This list shows the progress notes by %s.\n'
190 '\n'
191 ) % gmPerson.gmCurrentProvider()['short_alias'],
192 columns = [_('when'), _('type'), _('entry')],
193 single_selection = True,
194 can_return_empty = False,
195 edit_callback = edit,
196 delete_callback = delete,
197 refresh_callback = refresh,
198 ignore_OK_button = True
199 )
200
202
203 if parent is None:
204 parent = wx.GetApp().GetTopWindow()
205
206 searcher = wx.TextEntryDialog (
207 parent = parent,
208 message = _('Enter (regex) term to search for across all EMRs:'),
209 caption = _('Text search across all EMRs'),
210 style = wx.OK | wx.CANCEL | wx.CENTRE
211 )
212 result = searcher.ShowModal()
213
214 if result != wx.ID_OK:
215 return
216
217 wx.BeginBusyCursor()
218 term = searcher.GetValue()
219 searcher.Destroy()
220 results = gmClinNarrative.search_text_across_emrs(search_term = term)
221 wx.EndBusyCursor()
222
223 if len(results) == 0:
224 gmGuiHelpers.gm_show_info (
225 _(
226 'Nothing found for search term:\n'
227 ' "%s"'
228 ) % term,
229 _('Search results')
230 )
231 return
232
233 items = [ [gmPerson.cIdentity(aPK_obj = r['pk_patient'])['description_gender'], r['narrative'], r['src_table']] for r in results ]
234
235 selected_patient = gmListWidgets.get_choices_from_list (
236 parent = parent,
237 caption = _('Search results for %s') % term,
238 choices = items,
239 columns = [_('Patient'), _('Match'), _('Match location')],
240 data = [ r['pk_patient'] for r in results ],
241 single_selection = True,
242 can_return_empty = False
243 )
244
245 if selected_patient is None:
246 return
247
248 wx.CallAfter(gmPatSearchWidgets.set_active_patient, patient = gmPerson.cIdentity(aPK_obj = selected_patient))
249
251
252
253 if patient is None:
254 patient = gmPerson.gmCurrentPatient()
255
256 if not patient.connected:
257 gmDispatcher.send(signal = 'statustext', msg = _('Cannot search EMR. No active patient.'))
258 return False
259
260 if parent is None:
261 parent = wx.GetApp().GetTopWindow()
262
263 searcher = wx.TextEntryDialog (
264 parent = parent,
265 message = _('Enter search term:'),
266 caption = _('Text search of entire EMR of active patient'),
267 style = wx.OK | wx.CANCEL | wx.CENTRE
268 )
269 result = searcher.ShowModal()
270
271 if result != wx.ID_OK:
272 searcher.Destroy()
273 return False
274
275 wx.BeginBusyCursor()
276 val = searcher.GetValue()
277 searcher.Destroy()
278 emr = patient.get_emr()
279 rows = emr.search_narrative_simple(val)
280 wx.EndBusyCursor()
281
282 if len(rows) == 0:
283 gmGuiHelpers.gm_show_info (
284 _(
285 'Nothing found for search term:\n'
286 ' "%s"'
287 ) % val,
288 _('Search results')
289 )
290 return True
291
292 txt = u''
293 for row in rows:
294 txt += u'%s: %s\n' % (
295 row['soap_cat'],
296 row['narrative']
297 )
298
299 txt += u' %s: %s - %s %s\n' % (
300 _('Encounter'),
301 row['encounter_started'].strftime('%x %H:%M'),
302 row['encounter_ended'].strftime('%H:%M'),
303 row['encounter_type']
304 )
305 txt += u' %s: %s\n' % (
306 _('Episode'),
307 row['episode']
308 )
309 txt += u' %s: %s\n\n' % (
310 _('Health issue'),
311 row['health_issue']
312 )
313
314 msg = _(
315 'Search term was: "%s"\n'
316 '\n'
317 'Search results:\n\n'
318 '%s\n'
319 ) % (val, txt)
320
321 dlg = wx.MessageDialog (
322 parent = parent,
323 message = msg,
324 caption = _('Search results for %s') % val,
325 style = wx.OK | wx.STAY_ON_TOP
326 )
327 dlg.ShowModal()
328 dlg.Destroy()
329
330 return True
331
333
334
335 pat = gmPerson.gmCurrentPatient()
336 if not pat.connected:
337 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR for Medistar. No active patient.'))
338 return False
339
340 if encounter is None:
341 encounter = pat.get_emr().active_encounter
342
343 if parent is None:
344 parent = wx.GetApp().GetTopWindow()
345
346
347 aWildcard = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files"))
348
349 aDefDir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed','export')))
350
351 fname = '%s-%s-%s-%s-%s.txt' % (
352 'Medistar-MD',
353 time.strftime('%Y-%m-%d',time.localtime()),
354 pat['lastnames'].replace(' ', '-'),
355 pat['firstnames'].replace(' ', '_'),
356 pat.get_formatted_dob(format = '%Y-%m-%d')
357 )
358 dlg = wx.FileDialog (
359 parent = parent,
360 message = _("Save EMR extract for MEDISTAR import as..."),
361 defaultDir = aDefDir,
362 defaultFile = fname,
363 wildcard = aWildcard,
364 style = wx.SAVE
365 )
366 choice = dlg.ShowModal()
367 fname = dlg.GetPath()
368 dlg.Destroy()
369 if choice != wx.ID_OK:
370 return False
371
372 wx.BeginBusyCursor()
373 _log.debug('exporting encounter for medistar import to [%s]', fname)
374 exporter = gmPatientExporter.cMedistarSOAPExporter()
375 successful, fname = exporter.export_to_file (
376 filename = fname,
377 encounter = encounter,
378 soap_cats = u'soap',
379 export_to_import_file = True
380 )
381 if not successful:
382 gmGuiHelpers.gm_show_error (
383 _('Error exporting progress notes for MEDISTAR import.'),
384 _('MEDISTAR progress notes export')
385 )
386 wx.EndBusyCursor()
387 return False
388
389 gmDispatcher.send(signal = 'statustext', msg = _('Successfully exported progress notes into file [%s] for Medistar import.') % fname, beep=False)
390
391 wx.EndBusyCursor()
392 return True
393
395 """soap_cats needs to be a list"""
396
397 pat = gmPerson.gmCurrentPatient()
398 emr = pat.get_emr()
399
400 if parent is None:
401 parent = wx.GetApp().GetTopWindow()
402
403 selected_soap = {}
404 selected_issue_pks = []
405 selected_episode_pks = []
406 selected_narrative_pks = []
407
408 while 1:
409
410 all_issues = emr.get_health_issues()
411 all_issues.insert(0, gmEMRStructItems.get_dummy_health_issue())
412 dlg = gmEMRStructWidgets.cIssueListSelectorDlg (
413 parent = parent,
414 id = -1,
415 issues = all_issues,
416 msg = _('\n In the list below mark the health issues you want to report on.\n')
417 )
418 selection_idxs = []
419 for idx in range(len(all_issues)):
420 if all_issues[idx]['pk_health_issue'] in selected_issue_pks:
421 selection_idxs.append(idx)
422 if len(selection_idxs) != 0:
423 dlg.set_selections(selections = selection_idxs)
424 btn_pressed = dlg.ShowModal()
425 selected_issues = dlg.get_selected_item_data()
426 dlg.Destroy()
427
428 if btn_pressed == wx.ID_CANCEL:
429 return selected_soap.values()
430
431 selected_issue_pks = [ i['pk_health_issue'] for i in selected_issues ]
432
433 while 1:
434
435 all_epis = emr.get_episodes(issues = selected_issue_pks)
436
437 if len(all_epis) == 0:
438 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.'))
439 break
440
441 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg (
442 parent = parent,
443 id = -1,
444 episodes = all_epis,
445 msg = _(
446 '\n These are the episodes known for the health issues just selected.\n\n'
447 ' Now, mark the the episodes you want to report on.\n'
448 )
449 )
450 selection_idxs = []
451 for idx in range(len(all_epis)):
452 if all_epis[idx]['pk_episode'] in selected_episode_pks:
453 selection_idxs.append(idx)
454 if len(selection_idxs) != 0:
455 dlg.set_selections(selections = selection_idxs)
456 btn_pressed = dlg.ShowModal()
457 selected_epis = dlg.get_selected_item_data()
458 dlg.Destroy()
459
460 if btn_pressed == wx.ID_CANCEL:
461 break
462
463 selected_episode_pks = [ i['pk_episode'] for i in selected_epis ]
464
465
466 all_narr = emr.get_clin_narrative(episodes = selected_episode_pks, soap_cats = soap_cats)
467
468 if len(all_narr) == 0:
469 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episodes.'))
470 continue
471
472 dlg = cNarrativeListSelectorDlg (
473 parent = parent,
474 id = -1,
475 narrative = all_narr,
476 msg = _(
477 '\n This is the narrative (type %s) for the chosen episodes.\n\n'
478 ' Now, mark the entries you want to include in your report.\n'
479 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soap')) ])
480 )
481 selection_idxs = []
482 for idx in range(len(all_narr)):
483 if all_narr[idx]['pk_narrative'] in selected_narrative_pks:
484 selection_idxs.append(idx)
485 if len(selection_idxs) != 0:
486 dlg.set_selections(selections = selection_idxs)
487 btn_pressed = dlg.ShowModal()
488 selected_narr = dlg.get_selected_item_data()
489 dlg.Destroy()
490
491 if btn_pressed == wx.ID_CANCEL:
492 continue
493
494 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ]
495 for narr in selected_narr:
496 selected_soap[narr['pk_narrative']] = narr
497
499
501
502 narrative = kwargs['narrative']
503 del kwargs['narrative']
504
505 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs)
506
507 self.SetTitle(_('Select the narrative you are interested in ...'))
508
509 self._LCTRL_items.set_columns([_('when'), _('who'), _('type'), _('entry')])
510
511 self._LCTRL_items.set_string_items (
512 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 ]
513 )
514 self._LCTRL_items.set_column_widths()
515 self._LCTRL_items.set_data(data = narrative)
516
518
520
521 self.encounter = kwargs['encounter']
522 self.source_episode = kwargs['episode']
523 del kwargs['encounter']
524 del kwargs['episode']
525
526 wxgMoveNarrativeDlg.wxgMoveNarrativeDlg.__init__(self, *args, **kwargs)
527
528 self.LBL_source_episode.SetLabel(u'%s%s' % (self.source_episode['description'], gmTools.coalesce(self.source_episode['health_issue'], u'', u' (%s)')))
529 self.LBL_encounter.SetLabel('%s: %s %s - %s' % (
530 self.encounter['started'].strftime('%x').decode(gmI18N.get_encoding()),
531 self.encounter['l10n_type'],
532 self.encounter['started'].strftime('%H:%M'),
533 self.encounter['last_affirmed'].strftime('%H:%M')
534 ))
535 pat = gmPerson.gmCurrentPatient()
536 emr = pat.get_emr()
537 narr = emr.get_clin_narrative(episodes=[self.source_episode['pk_episode']], encounters=[self.encounter['pk_encounter']])
538 if len(narr) == 0:
539 narr = [{'narrative': _('There is no narrative for this episode in this encounter.')}]
540 self.LBL_narrative.SetLabel(u'\n'.join([n['narrative'] for n in narr]))
541
542
564
565 from Gnumed.wxGladeWidgets import wxgSoapPluginPnl
566
567 -class cSoapPluginPnl(wxgSoapPluginPnl.wxgSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
568 """A panel for in-context editing of progress notes.
569
570 Expects to be used as a notebook page.
571
572 Left hand side:
573 - problem list (health issues and active episodes)
574 - hints area
575
576 Right hand side:
577 - previous notes
578 - notebook with progress note editors
579 - encounter details fields
580
581 Listens to patient change signals, thus acts on the current patient.
582 """
593
594
595
597
598 if not self.__encounter_valid_for_save():
599 return False
600
601 emr = self.__pat.get_emr()
602 enc = emr.active_encounter
603
604 enc['pk_type'] = self._PRW_encounter_type.GetData()
605 enc['started'] = self._PRW_encounter_start.GetData().get_pydt()
606 enc['last_affirmed'] = self._PRW_encounter_end.GetData().get_pydt()
607 rfe = self._TCTRL_rfe.GetValue().strip()
608 if len(rfe) == 0:
609 enc['reason_for_encounter'] = None
610 else:
611 enc['reason_for_encounter'] = rfe
612 aoe = self._TCTRL_aoe.GetValue().strip()
613 if len(aoe) == 0:
614 enc['assessment_of_encounter'] = None
615 else:
616 enc['assessment_of_encounter'] = aoe
617
618 enc.save_payload()
619
620 return True
621
622
623
625 self._LCTRL_active_problems.set_columns([_('Last'), _('Problem'), _('Health issue')])
626 self._LCTRL_active_problems.set_string_items()
627
628 self._splitter_main.SetSashGravity(0.5)
629 self._splitter_left.SetSashGravity(0.5)
630 self._splitter_right.SetSashGravity(1.0)
631
632 splitter_size = self._splitter_main.GetSizeTuple()[0]
633 self._splitter_main.SetSashPosition(splitter_size * 3 / 10, True)
634
635 splitter_size = self._splitter_left.GetSizeTuple()[1]
636 self._splitter_left.SetSashPosition(splitter_size * 6 / 20, True)
637
638 splitter_size = self._splitter_right.GetSizeTuple()[1]
639 self._splitter_right.SetSashPosition(splitter_size * 15 / 20, True)
640
641 self._NB_soap_editors.DeleteAllPages()
642
644 """
645 Clear all information from input panel
646 """
647 self._LCTRL_active_problems.set_string_items()
648 self._lbl_hints.SetLabel(u'')
649 self._TCTRL_recent_notes.SetValue(u'')
650 self._NB_soap_editors.DeleteAllPages()
651 self._NB_soap_editors.add_editor()
652 self._PRW_encounter_type.SetText(suppress_smarts = True)
653 self._PRW_encounter_start.SetText(suppress_smarts = True)
654 self._PRW_encounter_end.SetText(suppress_smarts = True)
655 self._TCTRL_rfe.SetValue(u'')
656 self._TCTRL_aoe.SetValue(u'')
657
659 """Update health problems list.
660 """
661
662 self._LCTRL_active_problems.set_string_items()
663
664 emr = self.__pat.get_emr()
665 problems = emr.get_problems (
666 include_closed_episodes = self._CHBOX_show_closed_episodes.IsChecked(),
667 include_irrelevant_issues = self._CHBOX_irrelevant_issues.IsChecked()
668 )
669
670 list_items = []
671 active_problems = []
672 for problem in problems:
673 if not problem['problem_active']:
674 if not problem['is_potential_problem']:
675 continue
676
677 active_problems.append(problem)
678
679 if problem['type'] == 'issue':
680 issue = emr.problem2issue(problem)
681 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue'])
682 if last_encounter is None:
683 last = issue['modified_when'].strftime('%m/%Y')
684 else:
685 last = last_encounter['last_affirmed'].strftime('%m/%Y')
686
687 list_items.append([last, problem['problem'], gmTools.u_left_arrow])
688
689 elif problem['type'] == 'episode':
690 epi = emr.problem2episode(problem)
691 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode'])
692 if last_encounter is None:
693 last = epi['episode_modified_when'].strftime('%m/%Y')
694 else:
695 last = last_encounter['last_affirmed'].strftime('%m/%Y')
696
697 list_items.append ([
698 last,
699 problem['problem'],
700 gmTools.coalesce(initial = epi['health_issue'], instead = gmTools.u_diameter)
701 ])
702
703 self._LCTRL_active_problems.set_string_items(items = list_items)
704 self._LCTRL_active_problems.set_column_widths()
705 self._LCTRL_active_problems.set_data(data = active_problems)
706
707 showing_potential_problems = (
708 self._CHBOX_show_closed_episodes.IsChecked()
709 or
710 self._CHBOX_irrelevant_issues.IsChecked()
711 )
712 if showing_potential_problems:
713 self._SZR_problem_list_staticbox.SetLabel(_('%s (active+potential) problems') % len(list_items))
714 else:
715 self._SZR_problem_list_staticbox.SetLabel(_('%s active problems') % len(list_items))
716
717 return True
718
720 """This refreshes the recent-notes part."""
721
722 if problem is None:
723 soap = u''
724 caption = u'<?>'
725
726 elif problem['type'] == u'issue':
727 emr = self.__pat.get_emr()
728 soap = u''
729 caption = problem['problem'][:35]
730
731 prev_enc = emr.get_last_but_one_encounter(issue_id = problem['pk_health_issue'])
732 if prev_enc is not None:
733 soap += prev_enc.format (
734 with_soap = True,
735 with_docs = False,
736 with_tests = False,
737 patient = self.__pat,
738 issues = [ problem['pk_health_issue'] ],
739 fancy_header = False
740 )
741
742 tmp = emr.active_encounter.format_soap (
743 soap_cats = 'soap',
744 emr = emr,
745 issues = [ problem['pk_health_issue'] ],
746 )
747 if len(tmp) > 0:
748 soap += _('Current encounter:') + u'\n'
749 soap += u'\n'.join(tmp) + u'\n'
750
751 elif problem['type'] == u'episode':
752 emr = self.__pat.get_emr()
753 soap = u''
754 caption = problem['problem'][:35]
755
756 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_episode'])
757 if prev_enc is None:
758 if problem['pk_health_issue'] is not None:
759 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_health_issue'])
760 if prev_enc is not None:
761 soap += prev_enc.format (
762 with_soap = True,
763 with_docs = False,
764 with_tests = False,
765 patient = self.__pat,
766 issues = [ problem['pk_health_issue'] ],
767 fancy_header = False
768 )
769 else:
770 soap += prev_enc.format (
771 episodes = [ problem['pk_episode'] ],
772 with_soap = True,
773 with_docs = False,
774 with_tests = False,
775 patient = self.__pat,
776 fancy_header = False
777 )
778
779 tmp = emr.active_encounter.format_soap (
780 soap_cats = 'soap',
781 emr = emr,
782 issues = [ problem['pk_health_issue'] ],
783 )
784 if len(tmp) > 0:
785 soap += _('Current encounter:') + u'\n'
786 soap += u'\n'.join(tmp) + u'\n'
787
788 else:
789 soap = u''
790 caption = u'<?>'
791
792 self._TCTRL_recent_notes.SetValue(soap)
793 self._TCTRL_recent_notes.ShowPosition(self._TCTRL_recent_notes.GetLastPosition())
794 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on %s%s%s') % (
795 gmTools.u_left_double_angle_quote,
796 caption,
797 gmTools.u_right_double_angle_quote
798 ))
799
800 self._TCTRL_recent_notes.Refresh()
801
802 return True
803
831
833 """Assumes that the field data is valid."""
834
835 emr = self.__pat.get_emr()
836 enc = emr.active_encounter
837
838 data = {
839 'pk_type': self._PRW_encounter_type.GetData(),
840 'reason_for_encounter': gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u''),
841 'assessment_of_encounter': gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
842 'pk_location': enc['pk_location']
843 }
844
845 if self._PRW_encounter_start.GetData() is None:
846 data['started'] = None
847 else:
848 data['started'] = self._PRW_encounter_start.GetData().get_pydt()
849
850 if self._PRW_encounter_end.GetData() is None:
851 data['last_affirmed'] = None
852 else:
853 data['last_affirmed'] = self._PRW_encounter_end.GetData().get_pydt()
854
855 return enc.same_payload(another_object = data)
856
858
859 found_error = False
860
861 if self._PRW_encounter_type.GetData() is None:
862 found_error = True
863 msg = _('Cannot save encounter: missing type.')
864
865 if self._PRW_encounter_start.GetData() is None:
866 found_error = True
867 msg = _('Cannot save encounter: missing start time.')
868
869 if self._PRW_encounter_end.GetData() is None:
870 found_error = True
871 msg = _('Cannot save encounter: missing end time.')
872
873 if found_error:
874 gmDispatcher.send(signal = 'statustext', msg = msg, beep = True)
875 return False
876
877 return True
878
879
880
882 """Configure enabled event signals."""
883
884 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
885 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
886 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db)
887 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
888 gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified)
889 gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_modified)
890
891
892 self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback)
893 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
894
896 """Another patient is about to be activated.
897
898 Patient change will not proceed before this returns True.
899 """
900
901
902 if not self.__pat.connected:
903 return True
904 return self._NB_soap_editors.warn_on_unsaved_soap()
905
907 """The client is about to be shut down.
908
909 Shutdown will not proceed before this returns.
910 """
911 if not self.__pat.connected:
912 return True
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928 emr = self.__pat.get_emr()
929 if not self._NB_soap_editors.save_all_editors(emr = emr, rfe = self._TCTRL_rfe.GetValue().strip(), aoe = self._TCTRL_aoe.GetValue().strip()):
930 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
931 return True
932
934 wx.CallAfter(self.__on_pre_patient_selection)
935
937 self.__reset_ui_content()
938
940 wx.CallAfter(self._schedule_data_reget)
941
943 wx.CallAfter(self._schedule_data_reget)
944
946 wx.CallAfter(self.__refresh_encounter)
947
949 """Show related note at the bottom."""
950 pass
951
953 """Show related note at the bottom."""
954 emr = self.__pat.get_emr()
955 self.__refresh_recent_notes (
956 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
957 )
958
960 """Open progress note editor for this problem.
961 """
962 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
963 if problem is None:
964 return True
965
966 dbcfg = gmCfg.cCfgSQL()
967 allow_duplicate_editors = bool(dbcfg.get2 (
968 option = u'horstspace.soap_editor.allow_same_episode_multiple_times',
969 workplace = gmSurgery.gmCurrentPractice().active_workplace,
970 bias = u'user',
971 default = False
972 ))
973 if self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors):
974 return True
975
976 gmGuiHelpers.gm_show_error (
977 aMessage = _(
978 'Cannot open progress note editor for\n\n'
979 '[%s].\n\n'
980 ) % problem['problem'],
981 aTitle = _('opening progress note editor')
982 )
983 event.Skip()
984 return False
985
989
993
997
1004
1008
1017
1041
1043 self.__refresh_problem_list()
1044
1046 self.__refresh_problem_list()
1047
1048
1049
1051 self.__refresh_problem_list()
1052 self.__refresh_encounter()
1053 return True
1054
1218
1220
1222
1223 try:
1224 self.problem = kwargs['problem']
1225 del kwargs['problem']
1226 except KeyError:
1227 self.problem = None
1228
1229 wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl.__init__(self, *args, **kwargs)
1230
1231 self.fields = [
1232 self._TCTRL_Soap,
1233 self._TCTRL_sOap,
1234 self._TCTRL_soAp,
1235 self._TCTRL_soaP
1236 ]
1237
1238 self.__register_interests()
1239
1241 for field in self.fields:
1242 field.SetValue(u'')
1243
1244 - def save(self, emr=None, rfe=None, aoe=None):
1245
1246 if self.empty:
1247 return True
1248
1249
1250 if (self.problem is None) or (self.problem['type'] == 'issue'):
1251
1252 epi_name = gmTools.coalesce (
1253 aoe,
1254 gmTools.coalesce (
1255 rfe,
1256 u''
1257 )
1258 ).strip().replace('\r', '//').replace('\n', '//')
1259
1260 dlg = wx.TextEntryDialog (
1261 parent = self,
1262 message = _('Enter a short working name for this new problem:'),
1263 caption = _('Creating a problem (episode) to save the notelet under ...'),
1264 defaultValue = epi_name,
1265 style = wx.OK | wx.CANCEL | wx.CENTRE
1266 )
1267 decision = dlg.ShowModal()
1268 if decision != wx.ID_OK:
1269 return False
1270
1271 epi_name = dlg.GetValue().strip()
1272 if epi_name == u'':
1273 gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note'))
1274 return False
1275
1276
1277 new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True)
1278
1279 if self.problem is not None:
1280 issue = emr.problem2issue(self.problem)
1281 if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True):
1282 gmGuiHelpers.gm_show_warning (
1283 _(
1284 'The new episode:\n'
1285 '\n'
1286 ' "%s"\n'
1287 '\n'
1288 'will remain unassociated despite the editor\n'
1289 'having been invoked from the health issue:\n'
1290 '\n'
1291 ' "%s"'
1292 ) % (
1293 new_episode['description'],
1294 issue['description']
1295 ),
1296 _('saving progress note')
1297 )
1298
1299 epi_id = new_episode['pk_episode']
1300 else:
1301 epi_id = self.problem['pk_episode']
1302
1303 emr.add_notes(notes = self.soap, episode = epi_id)
1304
1305 return True
1306
1307
1308
1310 for field in self.fields:
1311 wxexpando.EVT_ETC_LAYOUT_NEEDED(field, field.GetId(), self._on_expando_needs_layout)
1312
1314
1315
1316
1317 self.Fit()
1318
1319 if self.HasScrollbar(wx.VERTICAL):
1320
1321 expando = self.FindWindowById(evt.GetId())
1322 y_expando = expando.GetPositionTuple()[1]
1323 h_expando = expando.GetSizeTuple()[1]
1324 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1
1325 y_cursor = int(round((float(line_cursor) / expando.NumberOfLines) * h_expando))
1326 y_desired_visible = y_expando + y_cursor
1327
1328 y_view = self.ViewStart[1]
1329 h_view = self.GetClientSizeTuple()[1]
1330
1331
1332
1333
1334
1335
1336
1337
1338 if y_desired_visible < y_view:
1339
1340 self.Scroll(0, y_desired_visible)
1341
1342 if y_desired_visible > h_view:
1343
1344 self.Scroll(0, y_desired_visible)
1345
1346
1347
1349 note = []
1350
1351 tmp = self._TCTRL_Soap.GetValue().strip()
1352 if tmp != u'':
1353 note.append(['s', tmp])
1354
1355 tmp = self._TCTRL_sOap.GetValue().strip()
1356 if tmp != u'':
1357 note.append(['o', tmp])
1358
1359 tmp = self._TCTRL_soAp.GetValue().strip()
1360 if tmp != u'':
1361 note.append(['a', tmp])
1362
1363 tmp = self._TCTRL_soaP.GetValue().strip()
1364 if tmp != u'':
1365 note.append(['p', tmp])
1366
1367 return note
1368
1369 soap = property(_get_soap, lambda x:x)
1370
1372 for field in self.fields:
1373 if field.GetValue().strip() != u'':
1374 return False
1375 return True
1376
1377 empty = property(_get_empty, lambda x:x)
1378
1379 -class cSoapLineTextCtrl(wxexpando.ExpandoTextCtrl):
1380
1381 - def __init__(self, *args, **kwargs):
1382
1383 wxexpando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
1384
1385 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+")
1386
1387 self.__register_interests()
1388
1389
1390
1392
1393
1394 wx.EVT_CHAR(self, self.__on_char)
1395 wx.EVT_SET_FOCUS(self, self.__on_focus)
1396
1397 - def __on_focus(self, evt):
1398 evt.Skip()
1399 wx.CallAfter(self._after_on_focus)
1400
1401 - def _after_on_focus(self):
1402 evt = wx.PyCommandEvent(wxexpando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
1403 evt.SetEventObject(self)
1404 evt.height = None
1405 evt.numLines = None
1406 self.GetEventHandler().ProcessEvent(evt)
1407
1408 - def __on_char(self, evt):
1409 char = unichr(evt.GetUnicodeKey())
1410
1411 if self.LastPosition == 1:
1412 evt.Skip()
1413 return
1414
1415 explicit_expansion = False
1416 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT):
1417 if evt.GetKeyCode() != 13:
1418 evt.Skip()
1419 return
1420 explicit_expansion = True
1421
1422 if not explicit_expansion:
1423 if self.__keyword_separators.match(char) is None:
1424 evt.Skip()
1425 return
1426
1427 caret_pos, line_no = self.PositionToXY(self.InsertionPoint)
1428 line = self.GetLineText(line_no)
1429 word = self.__keyword_separators.split(line[:caret_pos])[-1]
1430
1431 if (
1432 (not explicit_expansion)
1433 and
1434 (word != u'$$steffi')
1435 and
1436 (word not in [ r[0] for r in gmPG2.get_text_expansion_keywords() ])
1437 ):
1438 evt.Skip()
1439 return
1440
1441 start = self.InsertionPoint - len(word)
1442 wx.CallAfter(self.replace_keyword_with_expansion, word, start, explicit_expansion)
1443
1444 evt.Skip()
1445 return
1446
1447 - def replace_keyword_with_expansion(self, keyword=None, position=None, show_list=False):
1448
1449 if show_list:
1450 candidates = gmPG2.get_keyword_expansion_candidates(keyword = keyword)
1451 if len(candidates) == 0:
1452 return
1453 if len(candidates) == 1:
1454 keyword = candidates[0]
1455 else:
1456 keyword = gmListWidgets.get_choices_from_list (
1457 parent = self,
1458 msg = _(
1459 'Several macros match the keyword [%s].\n'
1460 '\n'
1461 'Please select the expansion you want to happen.'
1462 ) % keyword,
1463 caption = _('Selecting text macro'),
1464 choices = candidates,
1465 columns = [_('Keyword')],
1466 single_selection = True,
1467 can_return_empty = False
1468 )
1469 if keyword is None:
1470 return
1471
1472 expansion = gmPG2.expand_keyword(keyword = keyword)
1473
1474 if expansion is None:
1475 return
1476
1477 if expansion == u'':
1478 return
1479
1480 self.Replace (
1481 position,
1482 position + len(keyword),
1483 expansion
1484 )
1485
1486 self.SetInsertionPoint(position + len(expansion) + 1)
1487 self.ShowPosition(position + len(expansion) + 1)
1488
1489 return
1490
1491
1492
1493 if __name__ == '__main__':
1494
1495 gmI18N.activate_locale()
1496 gmI18N.install_domain(domain = 'gnumed')
1497
1498
1507
1514
1527
1528 if (len(sys.argv) > 1) and (sys.argv[1] == 'test'):
1529
1530 test_cSoapNoteExpandoEditAreaPnl()
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693