1 """GNUmed narrative handling widgets."""
2
3
4
5 __version__ = "$Revision: 1.45 $"
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 = _(
151 'This is the original progress note:\n'
152 '\n'
153 ' %s'
154 ) % item.format(left_margin = u' ', fancy = True),
155 text = item['narrative']
156 )
157 decision = dlg.ShowModal()
158
159 if decision != wx.ID_SAVE:
160 return False
161
162 val = dlg.value
163 dlg.Destroy()
164 if val.strip() == u'':
165 return False
166
167 item['narrative'] = val
168 item.save_payload()
169
170 return True
171
172 def refresh(lctrl):
173 notes = emr.get_clin_narrative (
174 encounters = encounters,
175 episodes = episodes,
176 providers = [ gmPerson.gmCurrentProvider()['short_alias'] ]
177 )
178 lctrl.set_string_items(items = [
179 [ narr['date'].strftime('%x %H:%M'),
180 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
181 narr['narrative'].replace('\n', '/').replace('\r', '/')
182 ] for narr in notes
183 ])
184 lctrl.set_data(data = notes)
185
186
187 gmListWidgets.get_choices_from_list (
188 parent = parent,
189 caption = _('Managing progress notes'),
190 msg = _(
191 '\n'
192 ' This list shows the progress notes by %s.\n'
193 '\n'
194 ) % gmPerson.gmCurrentProvider()['short_alias'],
195 columns = [_('when'), _('type'), _('entry')],
196 single_selection = True,
197 can_return_empty = False,
198 edit_callback = edit,
199 delete_callback = delete,
200 refresh_callback = refresh
201 )
202
204
205 if parent is None:
206 parent = wx.GetApp().GetTopWindow()
207
208 searcher = wx.TextEntryDialog (
209 parent = parent,
210 message = _('Enter (regex) term to search for across all EMRs:'),
211 caption = _('Text search across all EMRs'),
212 style = wx.OK | wx.CANCEL | wx.CENTRE
213 )
214 result = searcher.ShowModal()
215
216 if result != wx.ID_OK:
217 return
218
219 wx.BeginBusyCursor()
220 term = searcher.GetValue()
221 searcher.Destroy()
222 results = gmClinNarrative.search_text_across_emrs(search_term = term)
223 wx.EndBusyCursor()
224
225 if len(results) == 0:
226 gmGuiHelpers.gm_show_info (
227 _(
228 'Nothing found for search term:\n'
229 ' "%s"'
230 ) % term,
231 _('Search results')
232 )
233 return
234
235 items = [ [gmPerson.cIdentity(aPK_obj = r['pk_patient'])['description_gender'], r['narrative'], r['src_table']] for r in results ]
236
237 selected_patient = gmListWidgets.get_choices_from_list (
238 parent = parent,
239 caption = _('Search results for %s') % term,
240 choices = items,
241 columns = [_('Patient'), _('Match'), _('Match location')],
242 data = [ r['pk_patient'] for r in results ],
243 single_selection = True,
244 can_return_empty = False
245 )
246
247 if selected_patient is None:
248 return
249
250 wx.CallAfter(gmPatSearchWidgets.set_active_patient, patient = gmPerson.cIdentity(aPK_obj = selected_patient))
251
253
254
255 if patient is None:
256 patient = gmPerson.gmCurrentPatient()
257
258 if not patient.connected:
259 gmDispatcher.send(signal = 'statustext', msg = _('Cannot search EMR. No active patient.'))
260 return False
261
262 if parent is None:
263 parent = wx.GetApp().GetTopWindow()
264
265 searcher = wx.TextEntryDialog (
266 parent = parent,
267 message = _('Enter search term:'),
268 caption = _('Text search of entire EMR of active patient'),
269 style = wx.OK | wx.CANCEL | wx.CENTRE
270 )
271 result = searcher.ShowModal()
272
273 if result != wx.ID_OK:
274 searcher.Destroy()
275 return False
276
277 wx.BeginBusyCursor()
278 val = searcher.GetValue()
279 searcher.Destroy()
280 emr = patient.get_emr()
281 rows = emr.search_narrative_simple(val)
282 wx.EndBusyCursor()
283
284 if len(rows) == 0:
285 gmGuiHelpers.gm_show_info (
286 _(
287 'Nothing found for search term:\n'
288 ' "%s"'
289 ) % val,
290 _('Search results')
291 )
292 return True
293
294 txt = u''
295 for row in rows:
296 txt += u'%s: %s\n' % (
297 row['soap_cat'],
298 row['narrative']
299 )
300
301 txt += u' %s: %s - %s %s\n' % (
302 _('Encounter'),
303 row['encounter_started'].strftime('%x %H:%M'),
304 row['encounter_ended'].strftime('%H:%M'),
305 row['encounter_type']
306 )
307 txt += u' %s: %s\n' % (
308 _('Episode'),
309 row['episode']
310 )
311 txt += u' %s: %s\n\n' % (
312 _('Health issue'),
313 row['health_issue']
314 )
315
316 msg = _(
317 'Search term was: "%s"\n'
318 '\n'
319 'Search results:\n\n'
320 '%s\n'
321 ) % (val, txt)
322
323 dlg = wx.MessageDialog (
324 parent = parent,
325 message = msg,
326 caption = _('Search results for %s') % val,
327 style = wx.OK | wx.STAY_ON_TOP
328 )
329 dlg.ShowModal()
330 dlg.Destroy()
331
332 return True
333
335
336
337 pat = gmPerson.gmCurrentPatient()
338 if not pat.connected:
339 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR for Medistar. No active patient.'))
340 return False
341
342 if encounter is None:
343 encounter = pat.get_emr().active_encounter
344
345 if parent is None:
346 parent = wx.GetApp().GetTopWindow()
347
348
349 aWildcard = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files"))
350
351 aDefDir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed','export')))
352
353 fname = '%s-%s-%s-%s-%s.txt' % (
354 'Medistar-MD',
355 time.strftime('%Y-%m-%d',time.localtime()),
356 pat['lastnames'].replace(' ', '-'),
357 pat['firstnames'].replace(' ', '_'),
358 pat.get_formatted_dob(format = '%Y-%m-%d')
359 )
360 dlg = wx.FileDialog (
361 parent = parent,
362 message = _("Save EMR extract for MEDISTAR import as..."),
363 defaultDir = aDefDir,
364 defaultFile = fname,
365 wildcard = aWildcard,
366 style = wx.SAVE
367 )
368 choice = dlg.ShowModal()
369 fname = dlg.GetPath()
370 dlg.Destroy()
371 if choice != wx.ID_OK:
372 return False
373
374 wx.BeginBusyCursor()
375 _log.debug('exporting encounter for medistar import to [%s]', fname)
376 exporter = gmPatientExporter.cMedistarSOAPExporter()
377 successful, fname = exporter.export_to_file (
378 filename = fname,
379 encounter = encounter,
380 soap_cats = u'soap',
381 export_to_import_file = True
382 )
383 if not successful:
384 gmGuiHelpers.gm_show_error (
385 _('Error exporting progress notes for MEDISTAR import.'),
386 _('MEDISTAR progress notes export')
387 )
388 wx.EndBusyCursor()
389 return False
390
391 gmDispatcher.send(signal = 'statustext', msg = _('Successfully exported progress notes into file [%s] for Medistar import.') % fname, beep=False)
392
393 wx.EndBusyCursor()
394 return True
395
397 """soap_cats needs to be a list"""
398
399 pat = gmPerson.gmCurrentPatient()
400 emr = pat.get_emr()
401
402 if parent is None:
403 parent = wx.GetApp().GetTopWindow()
404
405 selected_soap = {}
406 selected_issue_pks = []
407 selected_episode_pks = []
408 selected_narrative_pks = []
409
410 while 1:
411
412 all_issues = emr.get_health_issues()
413 all_issues.insert(0, gmEMRStructItems.get_dummy_health_issue())
414 dlg = gmEMRStructWidgets.cIssueListSelectorDlg (
415 parent = parent,
416 id = -1,
417 issues = all_issues,
418 msg = _('\n In the list below mark the health issues you want to report on.\n')
419 )
420 selection_idxs = []
421 for idx in range(len(all_issues)):
422 if all_issues[idx]['pk_health_issue'] in selected_issue_pks:
423 selection_idxs.append(idx)
424 if len(selection_idxs) != 0:
425 dlg.set_selections(selections = selection_idxs)
426 btn_pressed = dlg.ShowModal()
427 selected_issues = dlg.get_selected_item_data()
428 dlg.Destroy()
429
430 if btn_pressed == wx.ID_CANCEL:
431 return selected_soap.values()
432
433 selected_issue_pks = [ i['pk_health_issue'] for i in selected_issues ]
434
435 while 1:
436
437 all_epis = emr.get_episodes(issues = selected_issue_pks)
438
439 if len(all_epis) == 0:
440 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.'))
441 break
442
443 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg (
444 parent = parent,
445 id = -1,
446 episodes = all_epis,
447 msg = _(
448 '\n These are the episodes known for the health issues just selected.\n\n'
449 ' Now, mark the the episodes you want to report on.\n'
450 )
451 )
452 selection_idxs = []
453 for idx in range(len(all_epis)):
454 if all_epis[idx]['pk_episode'] in selected_episode_pks:
455 selection_idxs.append(idx)
456 if len(selection_idxs) != 0:
457 dlg.set_selections(selections = selection_idxs)
458 btn_pressed = dlg.ShowModal()
459 selected_epis = dlg.get_selected_item_data()
460 dlg.Destroy()
461
462 if btn_pressed == wx.ID_CANCEL:
463 break
464
465 selected_episode_pks = [ i['pk_episode'] for i in selected_epis ]
466
467
468 all_narr = emr.get_clin_narrative(episodes = selected_episode_pks, soap_cats = soap_cats)
469
470 if len(all_narr) == 0:
471 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episodes.'))
472 continue
473
474 dlg = cNarrativeListSelectorDlg (
475 parent = parent,
476 id = -1,
477 narrative = all_narr,
478 msg = _(
479 '\n This is the narrative (type %s) for the chosen episodes.\n\n'
480 ' Now, mark the entries you want to include in your report.\n'
481 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soap')) ])
482 )
483 selection_idxs = []
484 for idx in range(len(all_narr)):
485 if all_narr[idx]['pk_narrative'] in selected_narrative_pks:
486 selection_idxs.append(idx)
487 if len(selection_idxs) != 0:
488 dlg.set_selections(selections = selection_idxs)
489 btn_pressed = dlg.ShowModal()
490 selected_narr = dlg.get_selected_item_data()
491 dlg.Destroy()
492
493 if btn_pressed == wx.ID_CANCEL:
494 continue
495
496 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ]
497 for narr in selected_narr:
498 selected_soap[narr['pk_narrative']] = narr
499
501
503
504 narrative = kwargs['narrative']
505 del kwargs['narrative']
506
507 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs)
508
509 self.SetTitle(_('Select the narrative you are interested in ...'))
510
511 self._LCTRL_items.set_columns([_('when'), _('who'), _('type'), _('entry')])
512
513 self._LCTRL_items.set_string_items (
514 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 ]
515 )
516 self._LCTRL_items.set_column_widths()
517 self._LCTRL_items.set_data(data = narrative)
518
520
522
523 self.encounter = kwargs['encounter']
524 self.source_episode = kwargs['episode']
525 del kwargs['encounter']
526 del kwargs['episode']
527
528 wxgMoveNarrativeDlg.wxgMoveNarrativeDlg.__init__(self, *args, **kwargs)
529
530 self.LBL_source_episode.SetLabel(u'%s%s' % (self.source_episode['description'], gmTools.coalesce(self.source_episode['health_issue'], u'', u' (%s)')))
531 self.LBL_encounter.SetLabel('%s: %s %s - %s' % (
532 self.encounter['started'].strftime('%x').decode(gmI18N.get_encoding()),
533 self.encounter['l10n_type'],
534 self.encounter['started'].strftime('%H:%M'),
535 self.encounter['last_affirmed'].strftime('%H:%M')
536 ))
537 pat = gmPerson.gmCurrentPatient()
538 emr = pat.get_emr()
539 narr = emr.get_clin_narrative(episodes=[self.source_episode['pk_episode']], encounters=[self.encounter['pk_encounter']])
540 if len(narr) == 0:
541 narr = [{'narrative': _('There is no narrative for this episode in this encounter.')}]
542 self.LBL_narrative.SetLabel(u'\n'.join([n['narrative'] for n in narr]))
543
544
566
567 from Gnumed.wxGladeWidgets import wxgSoapPluginPnl
568
569 -class cSoapPluginPnl(wxgSoapPluginPnl.wxgSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
570 """A panel for in-context editing of progress notes.
571
572 Expects to be used as a notebook page.
573
574 Left hand side:
575 - problem list (health issues and active episodes)
576 - hints area
577
578 Right hand side:
579 - previous notes
580 - notebook with progress note editors
581 - encounter details fields
582
583 Listens to patient change signals, thus acts on the current patient.
584 """
595
596
597
599
600 if not self.__encounter_valid_for_save():
601 return False
602
603 emr = self.__pat.get_emr()
604 enc = emr.active_encounter
605
606 enc['pk_type'] = self._PRW_encounter_type.GetData()
607 enc['started'] = self._PRW_encounter_start.GetData().get_pydt()
608 enc['last_affirmed'] = self._PRW_encounter_end.GetData().get_pydt()
609 rfe = self._TCTRL_rfe.GetValue().strip()
610 if len(rfe) == 0:
611 enc['reason_for_encounter'] = None
612 else:
613 enc['reason_for_encounter'] = rfe
614 aoe = self._TCTRL_aoe.GetValue().strip()
615 if len(aoe) == 0:
616 enc['assessment_of_encounter'] = None
617 else:
618 enc['assessment_of_encounter'] = aoe
619
620 enc.save_payload()
621
622 return True
623
624
625
627 self._LCTRL_active_problems.set_columns([_('Last'), _('Problem'), _('Health issue')])
628 self._LCTRL_active_problems.set_string_items()
629
630 self._splitter_main.SetSashGravity(0.5)
631 self._splitter_left.SetSashGravity(0.5)
632 self._splitter_right.SetSashGravity(1.0)
633
634 splitter_size = self._splitter_main.GetSizeTuple()[0]
635 self._splitter_main.SetSashPosition(splitter_size * 3 / 10, True)
636
637 splitter_size = self._splitter_left.GetSizeTuple()[1]
638 self._splitter_left.SetSashPosition(splitter_size * 6 / 20, True)
639
640 splitter_size = self._splitter_right.GetSizeTuple()[1]
641 self._splitter_right.SetSashPosition(splitter_size * 15 / 20, True)
642
643 self._NB_soap_editors.DeleteAllPages()
644
646 """
647 Clear all information from input panel
648 """
649 self._LCTRL_active_problems.set_string_items()
650 self._lbl_hints.SetLabel(u'')
651 self._TCTRL_recent_notes.SetValue(u'')
652 self._NB_soap_editors.DeleteAllPages()
653 self._NB_soap_editors.add_editor()
654 self._PRW_encounter_type.SetText(suppress_smarts = True)
655 self._PRW_encounter_start.SetText(suppress_smarts = True)
656 self._PRW_encounter_end.SetText(suppress_smarts = True)
657 self._TCTRL_rfe.SetValue(u'')
658 self._TCTRL_aoe.SetValue(u'')
659
661 """Update health problems list.
662 """
663
664 self._LCTRL_active_problems.set_string_items()
665
666 emr = self.__pat.get_emr()
667 problems = emr.get_problems (
668 include_closed_episodes = self._CHBOX_show_closed_episodes.IsChecked(),
669 include_irrelevant_issues = self._CHBOX_irrelevant_issues.IsChecked()
670 )
671
672 list_items = []
673 active_problems = []
674 for problem in problems:
675 if not problem['problem_active']:
676 if not problem['is_potential_problem']:
677 continue
678
679 active_problems.append(problem)
680
681 if problem['type'] == 'issue':
682 issue = emr.problem2issue(problem)
683 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue'])
684 if last_encounter is None:
685 last = issue['modified_when'].strftime('%m/%Y')
686 else:
687 last = last_encounter['last_affirmed'].strftime('%m/%Y')
688
689 list_items.append([last, problem['problem'], gmTools.u_left_arrow])
690
691 elif problem['type'] == 'episode':
692 epi = emr.problem2episode(problem)
693 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode'])
694 if last_encounter is None:
695 last = epi['episode_modified_when'].strftime('%m/%Y')
696 else:
697 last = last_encounter['last_affirmed'].strftime('%m/%Y')
698
699 list_items.append ([
700 last,
701 problem['problem'],
702 gmTools.coalesce(initial = epi['health_issue'], instead = gmTools.u_diameter)
703 ])
704
705 self._LCTRL_active_problems.set_string_items(items = list_items)
706 self._LCTRL_active_problems.set_column_widths()
707 self._LCTRL_active_problems.set_data(data = active_problems)
708
709 showing_potential_problems = (
710 self._CHBOX_show_closed_episodes.IsChecked()
711 or
712 self._CHBOX_irrelevant_issues.IsChecked()
713 )
714 if showing_potential_problems:
715 self._SZR_problem_list_staticbox.SetLabel(_('%s (active+potential) problems') % len(list_items))
716 else:
717 self._SZR_problem_list_staticbox.SetLabel(_('%s active problems') % len(list_items))
718
719 return True
720
722 """This refreshes the recent-notes part."""
723
724 if problem is None:
725 soap = u''
726 caption = u'<?>'
727
728 elif problem['type'] == u'issue':
729 emr = self.__pat.get_emr()
730 soap = u''
731 caption = problem['problem'][:35]
732
733 prev_enc = emr.get_last_but_one_encounter(issue_id = problem['pk_health_issue'])
734 if prev_enc is not None:
735 soap += prev_enc.format (
736 with_soap = True,
737 with_docs = False,
738 with_tests = False,
739 patient = self.__pat,
740 issues = [ problem['pk_health_issue'] ],
741 fancy_header = False
742 )
743
744 tmp = emr.active_encounter.format_soap (
745 soap_cats = 'soap',
746 emr = emr,
747 issues = [ problem['pk_health_issue'] ],
748 )
749 if len(tmp) > 0:
750 soap += _('Current encounter:') + u'\n'
751 soap += u'\n'.join(tmp) + u'\n'
752
753 elif problem['type'] == u'episode':
754 emr = self.__pat.get_emr()
755 soap = u''
756 caption = problem['problem'][:35]
757
758 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_episode'])
759 if prev_enc is None:
760 if problem['pk_health_issue'] is not None:
761 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_health_issue'])
762 if prev_enc is not None:
763 soap += prev_enc.format (
764 with_soap = True,
765 with_docs = False,
766 with_tests = False,
767 patient = self.__pat,
768 issues = [ problem['pk_health_issue'] ],
769 fancy_header = False
770 )
771 else:
772 soap += prev_enc.format (
773 episodes = [ problem['pk_episode'] ],
774 with_soap = True,
775 with_docs = False,
776 with_tests = False,
777 patient = self.__pat,
778 fancy_header = False
779 )
780
781 tmp = emr.active_encounter.format_soap (
782 soap_cats = 'soap',
783 emr = emr,
784 issues = [ problem['pk_health_issue'] ],
785 )
786 if len(tmp) > 0:
787 soap += _('Current encounter:') + u'\n'
788 soap += u'\n'.join(tmp) + u'\n'
789
790 else:
791 soap = u''
792 caption = u'<?>'
793
794 self._TCTRL_recent_notes.SetValue(soap)
795 self._TCTRL_recent_notes.ShowPosition(self._TCTRL_recent_notes.GetLastPosition())
796 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on %s%s%s') % (
797 gmTools.u_left_double_angle_quote,
798 caption,
799 gmTools.u_right_double_angle_quote
800 ))
801
802 self._TCTRL_recent_notes.Refresh()
803
804 return True
805
833
835 """Assumes that the field data is valid."""
836
837 emr = self.__pat.get_emr()
838 enc = emr.active_encounter
839
840 data = {
841 'pk_type': self._PRW_encounter_type.GetData(),
842 'reason_for_encounter': gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u''),
843 'assessment_of_encounter': gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
844 'pk_location': enc['pk_location']
845 }
846
847 if self._PRW_encounter_start.GetData() is None:
848 data['started'] = None
849 else:
850 data['started'] = self._PRW_encounter_start.GetData().get_pydt()
851
852 if self._PRW_encounter_end.GetData() is None:
853 data['last_affirmed'] = None
854 else:
855 data['last_affirmed'] = self._PRW_encounter_end.GetData().get_pydt()
856
857 return enc.same_payload(another_object = data)
858
860
861 found_error = False
862
863 if self._PRW_encounter_type.GetData() is None:
864 found_error = True
865 msg = _('Cannot save encounter: missing type.')
866
867 if self._PRW_encounter_start.GetData() is None:
868 found_error = True
869 msg = _('Cannot save encounter: missing start time.')
870
871 if self._PRW_encounter_end.GetData() is None:
872 found_error = True
873 msg = _('Cannot save encounter: missing end time.')
874
875 if found_error:
876 gmDispatcher.send(signal = 'statustext', msg = msg, beep = True)
877 return False
878
879 return True
880
881
882
884 """Configure enabled event signals."""
885
886 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
887 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
888 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db)
889 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
890 gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified)
891 gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_modified)
892
893
894 self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback)
895 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
896
898 """Another patient is about to be activated.
899
900 Patient change will not proceed before this returns True.
901 """
902
903
904 if not self.__pat.connected:
905 return True
906 return self._NB_soap_editors.warn_on_unsaved_soap()
907
909 """The client is about to be shut down.
910
911 Shutdown will not proceed before this returns.
912 """
913 if not self.__pat.connected:
914 return True
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930 emr = self.__pat.get_emr()
931 if not self._NB_soap_editors.save_all_editors(emr = emr, rfe = self._TCTRL_rfe.GetValue().strip(), aoe = self._TCTRL_aoe.GetValue().strip()):
932 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
933 return True
934
936 wx.CallAfter(self.__on_pre_patient_selection)
937
939 self.__reset_ui_content()
940
942 wx.CallAfter(self._schedule_data_reget)
943
945 wx.CallAfter(self._schedule_data_reget)
946
948 wx.CallAfter(self.__refresh_encounter)
949
951 """Show related note at the bottom."""
952 pass
953
955 """Show related note at the bottom."""
956 emr = self.__pat.get_emr()
957 self.__refresh_recent_notes (
958 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
959 )
960
962 """Open progress note editor for this problem.
963 """
964 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
965 if problem is None:
966 return True
967
968 dbcfg = gmCfg.cCfgSQL()
969 allow_duplicate_editors = bool(dbcfg.get2 (
970 option = u'horstspace.soap_editor.allow_same_episode_multiple_times',
971 workplace = gmSurgery.gmCurrentPractice().active_workplace,
972 bias = u'user',
973 default = False
974 ))
975 if self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors):
976 return True
977
978 gmGuiHelpers.gm_show_error (
979 aMessage = _(
980 'Cannot open progress note editor for\n\n'
981 '[%s].\n\n'
982 ) % problem['problem'],
983 aTitle = _('opening progress note editor')
984 )
985 event.Skip()
986 return False
987
991
995
999
1006
1010
1019
1043
1045 self.__refresh_problem_list()
1046
1048 self.__refresh_problem_list()
1049
1050
1051
1053 self.__refresh_problem_list()
1054 self.__refresh_encounter()
1055 return True
1056
1220
1222
1224
1225 try:
1226 self.problem = kwargs['problem']
1227 del kwargs['problem']
1228 except KeyError:
1229 self.problem = None
1230
1231 wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl.__init__(self, *args, **kwargs)
1232
1233 self.fields = [
1234 self._TCTRL_Soap,
1235 self._TCTRL_sOap,
1236 self._TCTRL_soAp,
1237 self._TCTRL_soaP
1238 ]
1239
1240 self.__register_interests()
1241
1243 for field in self.fields:
1244 field.SetValue(u'')
1245
1246 - def save(self, emr=None, rfe=None, aoe=None):
1247
1248 if self.empty:
1249 return True
1250
1251
1252 if (self.problem is None) or (self.problem['type'] == 'issue'):
1253
1254 epi_name = gmTools.coalesce (
1255 aoe,
1256 gmTools.coalesce (
1257 rfe,
1258 u''
1259 )
1260 ).strip().replace('\r', '//').replace('\n', '//')
1261
1262 dlg = wx.TextEntryDialog (
1263 parent = self,
1264 message = _('Enter a short working name for this new problem:'),
1265 caption = _('Creating a problem (episode) to save the notelet under ...'),
1266 defaultValue = epi_name,
1267 style = wx.OK | wx.CANCEL | wx.CENTRE
1268 )
1269 decision = dlg.ShowModal()
1270 if decision != wx.ID_OK:
1271 return False
1272
1273 epi_name = dlg.GetValue().strip()
1274 if epi_name == u'':
1275 gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note'))
1276 return False
1277
1278
1279 new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True)
1280
1281 if self.problem is not None:
1282 issue = emr.problem2issue(self.problem)
1283 if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True):
1284 gmGuiHelpers.gm_show_warning (
1285 _(
1286 'The new episode:\n'
1287 '\n'
1288 ' "%s"\n'
1289 '\n'
1290 'will remain unassociated despite the editor\n'
1291 'having been invoked from the health issue:\n'
1292 '\n'
1293 ' "%s"'
1294 ) % (
1295 new_episode['description'],
1296 issue['description']
1297 ),
1298 _('saving progress note')
1299 )
1300
1301 epi_id = new_episode['pk_episode']
1302 else:
1303 epi_id = self.problem['pk_episode']
1304
1305 emr.add_notes(notes = self.soap, episode = epi_id)
1306
1307 return True
1308
1309
1310
1312 for field in self.fields:
1313 wxexpando.EVT_ETC_LAYOUT_NEEDED(field, field.GetId(), self._on_expando_needs_layout)
1314
1316
1317
1318
1319 self.Fit()
1320
1321 if self.HasScrollbar(wx.VERTICAL):
1322
1323 expando = self.FindWindowById(evt.GetId())
1324 y_expando = expando.GetPositionTuple()[1]
1325 h_expando = expando.GetSizeTuple()[1]
1326 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1
1327 y_cursor = int(round((float(line_cursor) / expando.NumberOfLines) * h_expando))
1328 y_desired_visible = y_expando + y_cursor
1329
1330 y_view = self.ViewStart[1]
1331 h_view = self.GetClientSizeTuple()[1]
1332
1333
1334
1335
1336
1337
1338
1339
1340 if y_desired_visible < y_view:
1341
1342 self.Scroll(0, y_desired_visible)
1343
1344 if y_desired_visible > h_view:
1345
1346 self.Scroll(0, y_desired_visible)
1347
1348
1349
1351 note = []
1352
1353 tmp = self._TCTRL_Soap.GetValue().strip()
1354 if tmp != u'':
1355 note.append(['s', tmp])
1356
1357 tmp = self._TCTRL_sOap.GetValue().strip()
1358 if tmp != u'':
1359 note.append(['o', tmp])
1360
1361 tmp = self._TCTRL_soAp.GetValue().strip()
1362 if tmp != u'':
1363 note.append(['a', tmp])
1364
1365 tmp = self._TCTRL_soaP.GetValue().strip()
1366 if tmp != u'':
1367 note.append(['p', tmp])
1368
1369 return note
1370
1371 soap = property(_get_soap, lambda x:x)
1372
1374 for field in self.fields:
1375 if field.GetValue().strip() != u'':
1376 return False
1377 return True
1378
1379 empty = property(_get_empty, lambda x:x)
1380
1381 -class cSoapLineTextCtrl(wxexpando.ExpandoTextCtrl):
1382
1383 - def __init__(self, *args, **kwargs):
1384
1385 wxexpando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
1386
1387 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+")
1388
1389 self.__register_interests()
1390
1391
1392
1394
1395
1396 wx.EVT_CHAR(self, self.__on_char)
1397 wx.EVT_SET_FOCUS(self, self.__on_focus)
1398
1399 - def __on_focus(self, evt):
1400 evt.Skip()
1401 wx.CallAfter(self._after_on_focus)
1402
1403 - def _after_on_focus(self):
1404 evt = wx.PyCommandEvent(wxexpando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
1405 evt.SetEventObject(self)
1406 evt.height = None
1407 evt.numLines = None
1408 self.GetEventHandler().ProcessEvent(evt)
1409
1410 - def __on_char(self, evt):
1411 char = unichr(evt.GetUnicodeKey())
1412
1413 if self.LastPosition == 1:
1414 evt.Skip()
1415 return
1416
1417 explicit_expansion = False
1418 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT):
1419 if evt.GetKeyCode() != 13:
1420 evt.Skip()
1421 return
1422 explicit_expansion = True
1423
1424 if not explicit_expansion:
1425 if self.__keyword_separators.match(char) is None:
1426 evt.Skip()
1427 return
1428
1429 caret_pos, line_no = self.PositionToXY(self.InsertionPoint)
1430 line = self.GetLineText(line_no)
1431 word = self.__keyword_separators.split(line[:caret_pos])[-1]
1432
1433 if (
1434 (not explicit_expansion)
1435 and
1436 (word != u'$$steffi')
1437 and
1438 (word not in [ r[0] for r in gmPG2.get_text_expansion_keywords() ])
1439 ):
1440 evt.Skip()
1441 return
1442
1443 start = self.InsertionPoint - len(word)
1444 wx.CallAfter(self.replace_keyword_with_expansion, word, start, explicit_expansion)
1445
1446 evt.Skip()
1447 return
1448
1449 - def replace_keyword_with_expansion(self, keyword=None, position=None, show_list=False):
1450
1451 if show_list:
1452 candidates = gmPG2.get_keyword_expansion_candidates(keyword = keyword)
1453 if len(candidates) == 0:
1454 return
1455 if len(candidates) == 1:
1456 keyword = candidates[0]
1457 else:
1458 keyword = gmListWidgets.get_choices_from_list (
1459 parent = self,
1460 msg = _(
1461 'Several macros match the keyword [%s].\n'
1462 '\n'
1463 'Please select the expansion you want to happen.'
1464 ) % keyword,
1465 caption = _('Selecting text macro'),
1466 choices = candidates,
1467 columns = [_('Keyword')],
1468 single_selection = True,
1469 can_return_empty = False
1470 )
1471 if keyword is None:
1472 return
1473
1474 expansion = gmPG2.expand_keyword(keyword = keyword)
1475
1476 if expansion is None:
1477 return
1478
1479 if expansion == u'':
1480 return
1481
1482 self.Replace (
1483 position,
1484 position + len(keyword),
1485 expansion
1486 )
1487
1488 self.SetInsertionPoint(position + len(expansion) + 1)
1489 self.ShowPosition(position + len(expansion) + 1)
1490
1491 return
1492
1493
1494
1495 if __name__ == '__main__':
1496
1497 gmI18N.activate_locale()
1498 gmI18N.install_domain(domain = 'gnumed')
1499
1500
1509
1516
1529
1530 if (len(sys.argv) > 1) and (sys.argv[1] == 'test'):
1531
1532 test_cSoapNoteExpandoEditAreaPnl()
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