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 wx_expando
11 import wx.lib.agw.supertooltip as agw_stt
12 import wx.lib.statbmp as wx_genstatbmp
13
14
15 if __name__ == '__main__':
16 sys.path.insert(0, '../../')
17 from Gnumed.pycommon import gmI18N
18 from Gnumed.pycommon import gmDispatcher
19 from Gnumed.pycommon import gmTools
20 from Gnumed.pycommon import gmDateTime
21 from Gnumed.pycommon import gmShellAPI
22 from Gnumed.pycommon import gmPG2
23 from Gnumed.pycommon import gmCfg
24 from Gnumed.pycommon import gmMatchProvider
25
26 from Gnumed.business import gmPerson
27 from Gnumed.business import gmStaff
28 from Gnumed.business import gmEMRStructItems
29 from Gnumed.business import gmClinNarrative
30 from Gnumed.business import gmSurgery
31 from Gnumed.business import gmForms
32 from Gnumed.business import gmDocuments
33 from Gnumed.business import gmPersonSearch
34
35 from Gnumed.wxpython import gmListWidgets
36 from Gnumed.wxpython import gmEMRStructWidgets
37 from Gnumed.wxpython import gmRegetMixin
38 from Gnumed.wxpython import gmPhraseWheel
39 from Gnumed.wxpython import gmGuiHelpers
40 from Gnumed.wxpython import gmPatSearchWidgets
41 from Gnumed.wxpython import gmCfgWidgets
42 from Gnumed.wxpython import gmDocumentWidgets
43 from Gnumed.wxpython import gmTextExpansionWidgets
44
45 from Gnumed.exporters import gmPatientExporter
46
47
48 _log = logging.getLogger('gm.ui')
49 _log.info(__version__)
50
51
52
54
55
56 if patient is None:
57 patient = gmPerson.gmCurrentPatient()
58
59 if not patient.connected:
60 gmDispatcher.send(signal = 'statustext', msg = _('Cannot move progress notes. No active patient.'))
61 return False
62
63 if parent is None:
64 parent = wx.GetApp().GetTopWindow()
65
66 emr = patient.get_emr()
67
68 if encounters is None:
69 encs = emr.get_encounters(episodes = episodes)
70 encounters = gmEMRStructWidgets.select_encounters (
71 parent = parent,
72 patient = patient,
73 single_selection = False,
74 encounters = encs
75 )
76
77 if encounters is None:
78 return True
79
80 if len(encounters) == 0:
81 return True
82
83 notes = emr.get_clin_narrative (
84 encounters = encounters,
85 episodes = episodes
86 )
87
88
89 if move_all:
90 selected_narr = notes
91 else:
92 selected_narr = gmListWidgets.get_choices_from_list (
93 parent = parent,
94 caption = _('Moving progress notes between encounters ...'),
95 single_selection = False,
96 can_return_empty = True,
97 data = notes,
98 msg = _('\n Select the progress notes to move from the list !\n\n'),
99 columns = [_('when'), _('who'), _('type'), _('entry')],
100 choices = [
101 [ narr['date'].strftime('%x %H:%M'),
102 narr['provider'],
103 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
104 narr['narrative'].replace('\n', '/').replace('\r', '/')
105 ] for narr in notes
106 ]
107 )
108
109 if not selected_narr:
110 return True
111
112
113 enc2move2 = gmEMRStructWidgets.select_encounters (
114 parent = parent,
115 patient = patient,
116 single_selection = True
117 )
118
119 if not enc2move2:
120 return True
121
122 for narr in selected_narr:
123 narr['pk_encounter'] = enc2move2['pk_encounter']
124 narr.save()
125
126 return True
127
129
130
131 if patient is None:
132 patient = gmPerson.gmCurrentPatient()
133
134 if not patient.connected:
135 gmDispatcher.send(signal = 'statustext', msg = _('Cannot edit progress notes. No active patient.'))
136 return False
137
138 if parent is None:
139 parent = wx.GetApp().GetTopWindow()
140
141 emr = patient.get_emr()
142
143 def delete(item):
144 if item is None:
145 return False
146 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
147 parent,
148 -1,
149 caption = _('Deleting progress note'),
150 question = _(
151 'Are you positively sure you want to delete this\n'
152 'progress note from the medical record ?\n'
153 '\n'
154 'Note that even if you chose to delete the entry it will\n'
155 'still be (invisibly) kept in the audit trail to protect\n'
156 'you from litigation because physical deletion is known\n'
157 'to be unlawful in some jurisdictions.\n'
158 ),
159 button_defs = (
160 {'label': _('Delete'), 'tooltip': _('Yes, delete the progress note.'), 'default': False},
161 {'label': _('Cancel'), 'tooltip': _('No, do NOT delete the progress note.'), 'default': True}
162 )
163 )
164 decision = dlg.ShowModal()
165
166 if decision != wx.ID_YES:
167 return False
168
169 gmClinNarrative.delete_clin_narrative(narrative = item['pk_narrative'])
170 return True
171
172 def edit(item):
173 if item is None:
174 return False
175
176 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
177 parent,
178 -1,
179 title = _('Editing progress note'),
180 msg = _('This is the original progress note:'),
181 data = item.format(left_margin = u' ', fancy = True),
182 text = item['narrative']
183 )
184 decision = dlg.ShowModal()
185
186 if decision != wx.ID_SAVE:
187 return False
188
189 val = dlg.value
190 dlg.Destroy()
191 if val.strip() == u'':
192 return False
193
194 item['narrative'] = val
195 item.save_payload()
196
197 return True
198
199 def refresh(lctrl):
200 notes = emr.get_clin_narrative (
201 encounters = encounters,
202 episodes = episodes,
203 providers = [ gmStaff.gmCurrentProvider()['short_alias'] ]
204 )
205 lctrl.set_string_items(items = [
206 [ narr['date'].strftime('%x %H:%M'),
207 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
208 narr['narrative'].replace('\n', '/').replace('\r', '/')
209 ] for narr in notes
210 ])
211 lctrl.set_data(data = notes)
212
213
214 gmListWidgets.get_choices_from_list (
215 parent = parent,
216 caption = _('Managing progress notes'),
217 msg = _(
218 '\n'
219 ' This list shows the progress notes by %s.\n'
220 '\n'
221 ) % gmStaff.gmCurrentProvider()['short_alias'],
222 columns = [_('when'), _('type'), _('entry')],
223 single_selection = True,
224 can_return_empty = False,
225 edit_callback = edit,
226 delete_callback = delete,
227 refresh_callback = refresh
228 )
229
231
232 if parent is None:
233 parent = wx.GetApp().GetTopWindow()
234
235 searcher = wx.TextEntryDialog (
236 parent = parent,
237 message = _('Enter (regex) term to search for across all EMRs:'),
238 caption = _('Text search across all EMRs'),
239 style = wx.OK | wx.CANCEL | wx.CENTRE
240 )
241 result = searcher.ShowModal()
242
243 if result != wx.ID_OK:
244 return
245
246 wx.BeginBusyCursor()
247 term = searcher.GetValue()
248 searcher.Destroy()
249 results = gmClinNarrative.search_text_across_emrs(search_term = term)
250 wx.EndBusyCursor()
251
252 if len(results) == 0:
253 gmGuiHelpers.gm_show_info (
254 _(
255 'Nothing found for search term:\n'
256 ' "%s"'
257 ) % term,
258 _('Search results')
259 )
260 return
261
262 items = [ [gmPerson.cIdentity(aPK_obj =
263 r['pk_patient'])['description_gender'], r['narrative'],
264 r['src_table']] for r in results ]
265
266 selected_patient = gmListWidgets.get_choices_from_list (
267 parent = parent,
268 caption = _('Search results for %s') % term,
269 choices = items,
270 columns = [_('Patient'), _('Match'), _('Match location')],
271 data = [ r['pk_patient'] for r in results ],
272 single_selection = True,
273 can_return_empty = False
274 )
275
276 if selected_patient is None:
277 return
278
279 wx.CallAfter(gmPatSearchWidgets.set_active_patient, patient = gmPerson.cIdentity(aPK_obj = selected_patient))
280
282
283
284 if patient is None:
285 patient = gmPerson.gmCurrentPatient()
286
287 if not patient.connected:
288 gmDispatcher.send(signal = 'statustext', msg = _('Cannot search EMR. No active patient.'))
289 return False
290
291 if parent is None:
292 parent = wx.GetApp().GetTopWindow()
293
294 searcher = wx.TextEntryDialog (
295 parent = parent,
296 message = _('Enter search term:'),
297 caption = _('Text search of entire EMR of active patient'),
298 style = wx.OK | wx.CANCEL | wx.CENTRE
299 )
300 result = searcher.ShowModal()
301
302 if result != wx.ID_OK:
303 searcher.Destroy()
304 return False
305
306 wx.BeginBusyCursor()
307 val = searcher.GetValue()
308 searcher.Destroy()
309 emr = patient.get_emr()
310 rows = emr.search_narrative_simple(val)
311 wx.EndBusyCursor()
312
313 if len(rows) == 0:
314 gmGuiHelpers.gm_show_info (
315 _(
316 'Nothing found for search term:\n'
317 ' "%s"'
318 ) % val,
319 _('Search results')
320 )
321 return True
322
323 txt = u''
324 for row in rows:
325 txt += u'%s: %s\n' % (
326 row['soap_cat'],
327 row['narrative']
328 )
329
330 txt += u' %s: %s - %s %s\n' % (
331 _('Encounter'),
332 row['encounter_started'].strftime('%x %H:%M'),
333 row['encounter_ended'].strftime('%H:%M'),
334 row['encounter_type']
335 )
336 txt += u' %s: %s\n' % (
337 _('Episode'),
338 row['episode']
339 )
340 txt += u' %s: %s\n\n' % (
341 _('Health issue'),
342 row['health_issue']
343 )
344
345 msg = _(
346 'Search term was: "%s"\n'
347 '\n'
348 'Search results:\n\n'
349 '%s\n'
350 ) % (val, txt)
351
352 dlg = wx.MessageDialog (
353 parent = parent,
354 message = msg,
355 caption = _('Search results for %s') % val,
356 style = wx.OK | wx.STAY_ON_TOP
357 )
358 dlg.ShowModal()
359 dlg.Destroy()
360
361 return True
362
364
365
366 pat = gmPerson.gmCurrentPatient()
367 if not pat.connected:
368 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR for Medistar. No active patient.'))
369 return False
370
371 if encounter is None:
372 encounter = pat.get_emr().active_encounter
373
374 if parent is None:
375 parent = wx.GetApp().GetTopWindow()
376
377
378 aWildcard = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files"))
379
380 aDefDir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed','export')))
381
382 fname = '%s-%s-%s-%s-%s.txt' % (
383 'Medistar-MD',
384 time.strftime('%Y-%m-%d',time.localtime()),
385 pat['lastnames'].replace(' ', '-'),
386 pat['firstnames'].replace(' ', '_'),
387 pat.get_formatted_dob(format = '%Y-%m-%d')
388 )
389 dlg = wx.FileDialog (
390 parent = parent,
391 message = _("Save EMR extract for MEDISTAR import as..."),
392 defaultDir = aDefDir,
393 defaultFile = fname,
394 wildcard = aWildcard,
395 style = wx.SAVE
396 )
397 choice = dlg.ShowModal()
398 fname = dlg.GetPath()
399 dlg.Destroy()
400 if choice != wx.ID_OK:
401 return False
402
403 wx.BeginBusyCursor()
404 _log.debug('exporting encounter for medistar import to [%s]', fname)
405 exporter = gmPatientExporter.cMedistarSOAPExporter()
406 successful, fname = exporter.export_to_file (
407 filename = fname,
408 encounter = encounter,
409 soap_cats = u'soapu',
410 export_to_import_file = True
411 )
412 if not successful:
413 gmGuiHelpers.gm_show_error (
414 _('Error exporting progress notes for MEDISTAR import.'),
415 _('MEDISTAR progress notes export')
416 )
417 wx.EndBusyCursor()
418 return False
419
420 gmDispatcher.send(signal = 'statustext', msg = _('Successfully exported progress notes into file [%s] for Medistar import.') % fname, beep=False)
421
422 wx.EndBusyCursor()
423 return True
424
426 """soap_cats needs to be a list"""
427
428 if parent is None:
429 parent = wx.GetApp().GetTopWindow()
430
431 pat = gmPerson.gmCurrentPatient()
432 emr = pat.get_emr()
433
434 selected_soap = {}
435 selected_narrative_pks = []
436
437
438 def pick_soap_from_episode(episode):
439
440 narr_for_epi = emr.get_clin_narrative(episodes = [episode['pk_episode']], soap_cats = soap_cats)
441
442 if len(narr_for_epi) == 0:
443 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episode.'))
444 return True
445
446 dlg = cNarrativeListSelectorDlg (
447 parent = parent,
448 id = -1,
449 narrative = narr_for_epi,
450 msg = _(
451 '\n This is the narrative (type %s) for the chosen episodes.\n'
452 '\n'
453 ' Now, mark the entries you want to include in your report.\n'
454 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soapu')) ])
455 )
456
457
458
459
460
461
462 btn_pressed = dlg.ShowModal()
463 selected_narr = dlg.get_selected_item_data()
464 dlg.Destroy()
465
466 if btn_pressed == wx.ID_CANCEL:
467 return True
468
469 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ]
470 for narr in selected_narr:
471 selected_soap[narr['pk_narrative']] = narr
472
473 print "before returning from picking soap"
474
475 return True
476
477 selected_episode_pks = []
478
479 all_epis = [ epi for epi in emr.get_episodes() if epi.has_narrative ]
480
481 if len(all_epis) == 0:
482 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.'))
483 return []
484
485 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg (
486 parent = parent,
487 id = -1,
488 episodes = all_epis,
489 msg = _('\n Select the the episode you want to report on.\n')
490 )
491
492
493
494
495
496
497 dlg.left_extra_button = (
498 _('Pick SOAP'),
499 _('Pick SOAP entries from topmost selected episode'),
500 pick_soap_from_episode
501 )
502 btn_pressed = dlg.ShowModal()
503 dlg.Destroy()
504
505 if btn_pressed == wx.ID_CANCEL:
506 return None
507
508 return selected_soap.values()
509
511 """soap_cats needs to be a list"""
512
513 pat = gmPerson.gmCurrentPatient()
514 emr = pat.get_emr()
515
516 if parent is None:
517 parent = wx.GetApp().GetTopWindow()
518
519 selected_soap = {}
520 selected_issue_pks = []
521 selected_episode_pks = []
522 selected_narrative_pks = []
523
524 while 1:
525
526 all_issues = emr.get_health_issues()
527 all_issues.insert(0, gmEMRStructItems.get_dummy_health_issue())
528 dlg = gmEMRStructWidgets.cIssueListSelectorDlg (
529 parent = parent,
530 id = -1,
531 issues = all_issues,
532 msg = _('\n In the list below mark the health issues you want to report on.\n')
533 )
534 selection_idxs = []
535 for idx in range(len(all_issues)):
536 if all_issues[idx]['pk_health_issue'] in selected_issue_pks:
537 selection_idxs.append(idx)
538 if len(selection_idxs) != 0:
539 dlg.set_selections(selections = selection_idxs)
540 btn_pressed = dlg.ShowModal()
541 selected_issues = dlg.get_selected_item_data()
542 dlg.Destroy()
543
544 if btn_pressed == wx.ID_CANCEL:
545 return selected_soap.values()
546
547 selected_issue_pks = [ i['pk_health_issue'] for i in selected_issues ]
548
549 while 1:
550
551 all_epis = emr.get_episodes(issues = selected_issue_pks)
552
553 if len(all_epis) == 0:
554 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.'))
555 break
556
557 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg (
558 parent = parent,
559 id = -1,
560 episodes = all_epis,
561 msg = _(
562 '\n These are the episodes known for the health issues just selected.\n\n'
563 ' Now, mark the the episodes you want to report on.\n'
564 )
565 )
566 selection_idxs = []
567 for idx in range(len(all_epis)):
568 if all_epis[idx]['pk_episode'] in selected_episode_pks:
569 selection_idxs.append(idx)
570 if len(selection_idxs) != 0:
571 dlg.set_selections(selections = selection_idxs)
572 btn_pressed = dlg.ShowModal()
573 selected_epis = dlg.get_selected_item_data()
574 dlg.Destroy()
575
576 if btn_pressed == wx.ID_CANCEL:
577 break
578
579 selected_episode_pks = [ i['pk_episode'] for i in selected_epis ]
580
581
582 all_narr = emr.get_clin_narrative(episodes = selected_episode_pks, soap_cats = soap_cats)
583
584 if len(all_narr) == 0:
585 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episodes.'))
586 continue
587
588 dlg = cNarrativeListSelectorDlg (
589 parent = parent,
590 id = -1,
591 narrative = all_narr,
592 msg = _(
593 '\n This is the narrative (type %s) for the chosen episodes.\n\n'
594 ' Now, mark the entries you want to include in your report.\n'
595 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soapu')) ])
596 )
597 selection_idxs = []
598 for idx in range(len(all_narr)):
599 if all_narr[idx]['pk_narrative'] in selected_narrative_pks:
600 selection_idxs.append(idx)
601 if len(selection_idxs) != 0:
602 dlg.set_selections(selections = selection_idxs)
603 btn_pressed = dlg.ShowModal()
604 selected_narr = dlg.get_selected_item_data()
605 dlg.Destroy()
606
607 if btn_pressed == wx.ID_CANCEL:
608 continue
609
610 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ]
611 for narr in selected_narr:
612 selected_soap[narr['pk_narrative']] = narr
613
615
617
618 narrative = kwargs['narrative']
619 del kwargs['narrative']
620
621 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs)
622
623 self.SetTitle(_('Select the narrative you are interested in ...'))
624
625 self._LCTRL_items.set_columns([_('when'), _('who'), _('type'), _('entry')])
626
627 self._LCTRL_items.set_string_items (
628 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 ]
629 )
630 self._LCTRL_items.set_column_widths()
631 self._LCTRL_items.set_data(data = narrative)
632
633 from Gnumed.wxGladeWidgets import wxgMoveNarrativeDlg
634
636
638
639 self.encounter = kwargs['encounter']
640 self.source_episode = kwargs['episode']
641 del kwargs['encounter']
642 del kwargs['episode']
643
644 wxgMoveNarrativeDlg.wxgMoveNarrativeDlg.__init__(self, *args, **kwargs)
645
646 self.LBL_source_episode.SetLabel(u'%s%s' % (self.source_episode['description'], gmTools.coalesce(self.source_episode['health_issue'], u'', u' (%s)')))
647 self.LBL_encounter.SetLabel('%s: %s %s - %s' % (
648 self.encounter['started'].strftime('%x').decode(gmI18N.get_encoding()),
649 self.encounter['l10n_type'],
650 self.encounter['started'].strftime('%H:%M'),
651 self.encounter['last_affirmed'].strftime('%H:%M')
652 ))
653 pat = gmPerson.gmCurrentPatient()
654 emr = pat.get_emr()
655 narr = emr.get_clin_narrative(episodes=[self.source_episode['pk_episode']], encounters=[self.encounter['pk_encounter']])
656 if len(narr) == 0:
657 narr = [{'narrative': _('There is no narrative for this episode in this encounter.')}]
658 self.LBL_narrative.SetLabel(u'\n'.join([n['narrative'] for n in narr]))
659
660
682
683 from Gnumed.wxGladeWidgets import wxgSoapPluginPnl
684
685 -class cSoapPluginPnl(wxgSoapPluginPnl.wxgSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
686 """A panel for in-context editing of progress notes.
687
688 Expects to be used as a notebook page.
689
690 Left hand side:
691 - problem list (health issues and active episodes)
692 - previous notes
693
694 Right hand side:
695 - encounter details fields
696 - notebook with progress note editors
697 - visual progress notes
698 - hints
699
700 Listens to patient change signals, thus acts on the current patient.
701 """
713
714
715
717
718 if not self.__encounter_valid_for_save():
719 return False
720
721 emr = self.__pat.get_emr()
722 enc = emr.active_encounter
723
724 rfe = self._TCTRL_rfe.GetValue().strip()
725 if len(rfe) == 0:
726 enc['reason_for_encounter'] = None
727 else:
728 enc['reason_for_encounter'] = rfe
729 aoe = self._TCTRL_aoe.GetValue().strip()
730 if len(aoe) == 0:
731 enc['assessment_of_encounter'] = None
732 else:
733 enc['assessment_of_encounter'] = aoe
734
735 enc.save_payload()
736
737 enc.generic_codes_rfe = [ c['data'] for c in self._PRW_rfe_codes.GetData() ]
738 enc.generic_codes_aoe = [ c['data'] for c in self._PRW_aoe_codes.GetData() ]
739
740 return True
741
742
743
745 self._LCTRL_active_problems.set_columns([_('Last'), _('Problem'), _('In health issue')])
746 self._LCTRL_active_problems.set_string_items()
747
748 self._splitter_main.SetSashGravity(0.5)
749 self._splitter_left.SetSashGravity(0.5)
750
751 splitter_size = self._splitter_main.GetSizeTuple()[0]
752 self._splitter_main.SetSashPosition(splitter_size * 3 / 10, True)
753
754 splitter_size = self._splitter_left.GetSizeTuple()[1]
755 self._splitter_left.SetSashPosition(splitter_size * 6 / 20, True)
756
757 self._NB_soap_editors.DeleteAllPages()
758 self._NB_soap_editors.MoveAfterInTabOrder(self._PRW_aoe_codes)
759
761 start = self._PRW_encounter_start.GetData()
762 if start is None:
763 return
764 start = start.get_pydt()
765
766 end = self._PRW_encounter_end.GetData()
767 if end is None:
768 fts = gmDateTime.cFuzzyTimestamp (
769 timestamp = start,
770 accuracy = gmDateTime.acc_minutes
771 )
772 self._PRW_encounter_end.SetText(fts.format_accurately(), data = fts)
773 return
774 end = end.get_pydt()
775
776 if start > end:
777 end = end.replace (
778 year = start.year,
779 month = start.month,
780 day = start.day
781 )
782 fts = gmDateTime.cFuzzyTimestamp (
783 timestamp = end,
784 accuracy = gmDateTime.acc_minutes
785 )
786 self._PRW_encounter_end.SetText(fts.format_accurately(), data = fts)
787 return
788
789 emr = self.__pat.get_emr()
790 if start != emr.active_encounter['started']:
791 end = end.replace (
792 year = start.year,
793 month = start.month,
794 day = start.day
795 )
796 fts = gmDateTime.cFuzzyTimestamp (
797 timestamp = end,
798 accuracy = gmDateTime.acc_minutes
799 )
800 self._PRW_encounter_end.SetText(fts.format_accurately(), data = fts)
801 return
802
803 return
804
806 """Clear all information from input panel."""
807
808 self._LCTRL_active_problems.set_string_items()
809
810 self._TCTRL_recent_notes.SetValue(u'')
811 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on selected problem'))
812
813 self._TCTRL_rfe.SetValue(u'')
814 self._PRW_rfe_codes.SetText(suppress_smarts = True)
815 self._TCTRL_aoe.SetValue(u'')
816 self._PRW_aoe_codes.SetText(suppress_smarts = True)
817
818 self._NB_soap_editors.DeleteAllPages()
819 self._NB_soap_editors.add_editor()
820
822 """Update health problems list."""
823
824 self._LCTRL_active_problems.set_string_items()
825
826 emr = self.__pat.get_emr()
827 problems = emr.get_problems (
828 include_closed_episodes = self._CHBOX_show_closed_episodes.IsChecked(),
829 include_irrelevant_issues = self._CHBOX_irrelevant_issues.IsChecked()
830 )
831
832 list_items = []
833 active_problems = []
834 for problem in problems:
835 if not problem['problem_active']:
836 if not problem['is_potential_problem']:
837 continue
838
839 active_problems.append(problem)
840
841 if problem['type'] == 'issue':
842 issue = emr.problem2issue(problem)
843 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue'])
844 if last_encounter is None:
845 last = issue['modified_when'].strftime('%m/%Y')
846 else:
847 last = last_encounter['last_affirmed'].strftime('%m/%Y')
848
849 list_items.append([last, problem['problem'], gmTools.u_down_left_arrow])
850
851 elif problem['type'] == 'episode':
852 epi = emr.problem2episode(problem)
853 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode'])
854 if last_encounter is None:
855 last = epi['episode_modified_when'].strftime('%m/%Y')
856 else:
857 last = last_encounter['last_affirmed'].strftime('%m/%Y')
858
859 list_items.append ([
860 last,
861 problem['problem'],
862 gmTools.coalesce(initial = epi['health_issue'], instead = u'?')
863 ])
864
865 self._LCTRL_active_problems.set_string_items(items = list_items)
866 self._LCTRL_active_problems.set_column_widths()
867 self._LCTRL_active_problems.set_data(data = active_problems)
868
869 showing_potential_problems = (
870 self._CHBOX_show_closed_episodes.IsChecked()
871 or
872 self._CHBOX_irrelevant_issues.IsChecked()
873 )
874 if showing_potential_problems:
875 self._SZR_problem_list_staticbox.SetLabel(_('%s (active+potential) problems') % len(list_items))
876 else:
877 self._SZR_problem_list_staticbox.SetLabel(_('%s active problems') % len(list_items))
878
879 return True
880
882 soap = u''
883 emr = self.__pat.get_emr()
884 prev_enc = emr.get_last_but_one_encounter(issue_id = problem['pk_health_issue'])
885 if prev_enc is not None:
886 soap += prev_enc.format (
887 issues = [ problem['pk_health_issue'] ],
888 with_soap = True,
889 with_docs = False,
890 with_tests = False,
891 patient = self.__pat,
892 fancy_header = False,
893 with_rfe_aoe = True
894 )
895
896 tmp = emr.active_encounter.format_soap (
897 soap_cats = 'soapu',
898 emr = emr,
899 issues = [ problem['pk_health_issue'] ],
900 )
901 if len(tmp) > 0:
902 soap += _('Current encounter:') + u'\n'
903 soap += u'\n'.join(tmp) + u'\n'
904
905 if problem['summary'] is not None:
906 soap += u'\n-- %s ----------\n%s' % (
907 _('Cumulative summary'),
908 gmTools.wrap (
909 text = problem['summary'],
910 width = 45,
911 initial_indent = u' ',
912 subsequent_indent = u' '
913 ).strip('\n')
914 )
915
916 return soap
917
919 soap = u''
920 emr = self.__pat.get_emr()
921 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_episode'])
922 if prev_enc is not None:
923 soap += prev_enc.format (
924 episodes = [ problem['pk_episode'] ],
925 with_soap = True,
926 with_docs = False,
927 with_tests = False,
928 patient = self.__pat,
929 fancy_header = False,
930 with_rfe_aoe = True
931 )
932 else:
933 if problem['pk_health_issue'] is not None:
934 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_health_issue'])
935 if prev_enc is not None:
936 soap += prev_enc.format (
937 with_soap = True,
938 with_docs = False,
939 with_tests = False,
940 patient = self.__pat,
941 issues = [ problem['pk_health_issue'] ],
942 fancy_header = False,
943 with_rfe_aoe = True
944 )
945
946 tmp = emr.active_encounter.format_soap (
947 soap_cats = 'soapu',
948 emr = emr,
949 issues = [ problem['pk_health_issue'] ],
950 )
951 if len(tmp) > 0:
952 soap += _('Current encounter:') + u'\n'
953 soap += u'\n'.join(tmp) + u'\n'
954
955 if problem['summary'] is not None:
956 soap += u'\n-- %s ----------\n%s' % (
957 _('Cumulative summary'),
958 gmTools.wrap (
959 text = problem['summary'],
960 width = 45,
961 initial_indent = u' ',
962 subsequent_indent = u' '
963 ).strip('\n')
964 )
965
966 return soap
967
970
994
996 """This refreshes the recent-notes part."""
997
998 soap = u''
999 caption = u'<?>'
1000
1001 if problem['type'] == u'issue':
1002 caption = problem['problem'][:35]
1003 soap = self.__get_soap_for_issue_problem(problem = problem)
1004
1005 elif problem['type'] == u'episode':
1006 caption = problem['problem'][:35]
1007 soap = self.__get_soap_for_episode_problem(problem = problem)
1008
1009 self._TCTRL_recent_notes.SetValue(soap)
1010 self._TCTRL_recent_notes.ShowPosition(self._TCTRL_recent_notes.GetLastPosition())
1011 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on %s%s%s') % (
1012 gmTools.u_left_double_angle_quote,
1013 caption,
1014 gmTools.u_right_double_angle_quote
1015 ))
1016
1017 self._TCTRL_recent_notes.Refresh()
1018
1019 return True
1020
1039
1041 """Assumes that the field data is valid."""
1042
1043 emr = self.__pat.get_emr()
1044 enc = emr.active_encounter
1045
1046 data = {
1047 'pk_type': enc['pk_type'],
1048 'reason_for_encounter': gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u''),
1049 'assessment_of_encounter': gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
1050 'pk_location': enc['pk_location'],
1051 'pk_patient': enc['pk_patient'],
1052 'pk_generic_codes_rfe': self._PRW_rfe_codes.GetData(),
1053 'pk_generic_codes_aoe': self._PRW_aoe_codes.GetData(),
1054 'started': enc['started'],
1055 'last_affirmed': enc['last_affirmed']
1056 }
1057
1058 return not enc.same_payload(another_object = data)
1059
1062
1063
1064
1066 """Configure enabled event signals."""
1067
1068 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1069 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1070 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db)
1071 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
1072 gmDispatcher.connect(signal = u'episode_code_mod_db', receiver = self._on_episode_issue_mod_db)
1073 gmDispatcher.connect(signal = u'doc_mod_db', receiver = self._on_doc_mod_db)
1074 gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified)
1075 gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_switched)
1076 gmDispatcher.connect(signal = u'rfe_code_mod_db', receiver = self._on_encounter_code_modified)
1077 gmDispatcher.connect(signal = u'aoe_code_mod_db', receiver = self._on_encounter_code_modified)
1078
1079
1080 self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback)
1081 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
1082
1084 """Another patient is about to be activated.
1085
1086 Patient change will not proceed before this returns True.
1087 """
1088
1089
1090 if not self.__pat.connected:
1091 return True
1092 return self._NB_soap_editors.warn_on_unsaved_soap()
1093
1095 """The client is about to be shut down.
1096
1097 Shutdown will not proceed before this returns.
1098 """
1099 if not self.__pat.connected:
1100 return True
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116 emr = self.__pat.get_emr()
1117 saved = self._NB_soap_editors.save_all_editors (
1118 emr = emr,
1119 episode_name_candidates = [
1120 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
1121 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'')
1122 ]
1123 )
1124 if not saved:
1125 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
1126 return True
1127
1129 wx.CallAfter(self.__on_pre_patient_selection)
1130
1132 self.__reset_ui_content()
1133
1135 wx.CallAfter(self._schedule_data_reget)
1136 self.__patient_just_changed = True
1137
1139 wx.CallAfter(self.__refresh_current_editor)
1140
1142 wx.CallAfter(self._schedule_data_reget)
1143
1148
1150 wx.CallAfter(self.__refresh_encounter)
1151
1153 wx.CallAfter(self.__on_current_encounter_switched)
1154
1156 self.__refresh_encounter()
1157
1158
1159
1161 """Show related note at the bottom."""
1162 pass
1163
1175
1177 """Show related note at the bottom."""
1178 emr = self.__pat.get_emr()
1179 self.__refresh_recent_notes (
1180 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1181 )
1182
1184 """Open progress note editor for this problem.
1185 """
1186 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1187 if problem is None:
1188 return True
1189
1190 dbcfg = gmCfg.cCfgSQL()
1191 allow_duplicate_editors = bool(dbcfg.get2 (
1192 option = u'horstspace.soap_editor.allow_same_episode_multiple_times',
1193 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1194 bias = u'user',
1195 default = False
1196 ))
1197 if self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors):
1198 return True
1199
1200 gmGuiHelpers.gm_show_error (
1201 aMessage = _(
1202 'Cannot open progress note editor for\n\n'
1203 '[%s].\n\n'
1204 ) % problem['problem'],
1205 aTitle = _('opening progress note editor')
1206 )
1207 event.Skip()
1208 return False
1209
1211 self.__refresh_problem_list()
1212
1214 self.__refresh_problem_list()
1215
1216
1217
1221
1225
1229
1240
1261
1266
1267
1268
1272
1273
1274
1283
1295
1296
1297
1299 self.__refresh_problem_list()
1300 self.__refresh_encounter()
1301 self.__setup_initial_patient_editors()
1302 return True
1303
1487
1488 from Gnumed.wxGladeWidgets import wxgSoapNoteExpandoEditAreaPnl
1489
1491 """An Edit Area like panel for entering progress notes.
1492
1493 Subjective: Codes:
1494 expando text ctrl
1495 Objective: Codes:
1496 expando text ctrl
1497 Assessment: Codes:
1498 expando text ctrl
1499 Plan: Codes:
1500 expando text ctrl
1501 visual progress notes
1502 panel with images
1503 Episode synopsis: Codes:
1504 text ctrl
1505
1506 - knows the problem this edit area is about
1507 - can deal with issue or episode type problems
1508 """
1509
1511
1512 try:
1513 self.problem = kwargs['problem']
1514 del kwargs['problem']
1515 except KeyError:
1516 self.problem = None
1517
1518 wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl.__init__(self, *args, **kwargs)
1519
1520 self.soap_fields = [
1521 self._TCTRL_Soap,
1522 self._TCTRL_sOap,
1523 self._TCTRL_soAp,
1524 self._TCTRL_soaP
1525 ]
1526
1527 self.__init_ui()
1528 self.__register_interests()
1529
1536
1540
1542 self._TCTRL_episode_summary.SetValue(u'')
1543 self._PRW_episode_codes.SetText(u'', self._PRW_episode_codes.list2data_dict([]))
1544 self._LBL_summary.SetLabel(_('Episode synopsis'))
1545
1546
1547 if self.problem is None:
1548 return
1549
1550
1551 if self.problem['type'] == u'issue':
1552 return
1553
1554
1555 caption = _(u'Synopsis (%s)') % (
1556 gmDateTime.pydt_strftime (
1557 self.problem['modified_when'],
1558 format = '%B %Y',
1559 accuracy = gmDateTime.acc_days
1560 )
1561 )
1562 self._LBL_summary.SetLabel(caption)
1563
1564 if self.problem['summary'] is not None:
1565 self._TCTRL_episode_summary.SetValue(self.problem['summary'].strip())
1566
1567 val, data = self._PRW_episode_codes.generic_linked_codes2item_dict(self.problem.generic_codes)
1568 self._PRW_episode_codes.SetText(val, data)
1569
1589
1591 for field in self.soap_fields:
1592 field.SetValue(u'')
1593 self._TCTRL_episode_summary.SetValue(u'')
1594 self._LBL_summary.SetLabel(_('Episode synopsis'))
1595 self._PRW_episode_codes.SetText(u'', self._PRW_episode_codes.list2data_dict([]))
1596 self._PNL_visual_soap.clear()
1597
1599 fname, discard_unmodified = select_visual_progress_note_template(parent = self)
1600 if fname is None:
1601 return False
1602
1603 if self.problem is None:
1604 issue = None
1605 episode = None
1606 elif self.problem['type'] == 'issue':
1607 issue = self.problem['pk_health_issue']
1608 episode = None
1609 else:
1610 issue = self.problem['pk_health_issue']
1611 episode = gmEMRStructItems.problem2episode(self.problem)
1612
1613 wx.CallAfter (
1614 edit_visual_progress_note,
1615 filename = fname,
1616 episode = episode,
1617 discard_unmodified = discard_unmodified,
1618 health_issue = issue
1619 )
1620
1621 - def save(self, emr=None, episode_name_candidates=None, encounter=None):
1622
1623 if self.empty:
1624 return True
1625
1626
1627 if (self.problem is None) or (self.problem['type'] == 'issue'):
1628 episode = self.__create_new_episode(emr = emr, episode_name_candidates = episode_name_candidates)
1629
1630 if episode is None:
1631 return False
1632
1633 else:
1634 episode = emr.problem2episode(self.problem)
1635
1636 if encounter is None:
1637 encounter = emr.current_encounter['pk_encounter']
1638
1639 soap_notes = []
1640 for note in self.soap:
1641 saved, data = gmClinNarrative.create_clin_narrative (
1642 soap_cat = note[0],
1643 narrative = note[1],
1644 episode_id = episode['pk_episode'],
1645 encounter_id = encounter
1646 )
1647 if saved:
1648 soap_notes.append(data)
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661 if self.problem is not None:
1662 if self.problem['type'] == 'episode':
1663 episode['summary'] = self._TCTRL_episode_summary.GetValue().strip()
1664 episode.save()
1665
1666
1667 episode.generic_codes = [ d['data'] for d in self._PRW_episode_codes.GetData() ]
1668
1669 return True
1670
1671
1672
1674
1675 episode_name_candidates.append(self._TCTRL_episode_summary.GetValue().strip())
1676 for candidate in episode_name_candidates:
1677 if candidate is None:
1678 continue
1679 epi_name = candidate.strip().replace('\r', '//').replace('\n', '//')
1680 break
1681
1682 dlg = wx.TextEntryDialog (
1683 parent = self,
1684 message = _('Enter a short working name for this new problem:'),
1685 caption = _('Creating a problem (episode) to save the notelet under ...'),
1686 defaultValue = epi_name,
1687 style = wx.OK | wx.CANCEL | wx.CENTRE
1688 )
1689 decision = dlg.ShowModal()
1690 if decision != wx.ID_OK:
1691 return None
1692
1693 epi_name = dlg.GetValue().strip()
1694 if epi_name == u'':
1695 gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note'))
1696 return None
1697
1698
1699 new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True)
1700 new_episode['summary'] = self._TCTRL_episode_summary.GetValue().strip()
1701 new_episode.save()
1702
1703 if self.problem is not None:
1704 issue = emr.problem2issue(self.problem)
1705 if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True):
1706 gmGuiHelpers.gm_show_warning (
1707 _(
1708 'The new episode:\n'
1709 '\n'
1710 ' "%s"\n'
1711 '\n'
1712 'will remain unassociated despite the editor\n'
1713 'having been invoked from the health issue:\n'
1714 '\n'
1715 ' "%s"'
1716 ) % (
1717 new_episode['description'],
1718 issue['description']
1719 ),
1720 _('saving progress note')
1721 )
1722
1723 return new_episode
1724
1725
1726
1728 for field in self.soap_fields:
1729 wx_expando.EVT_ETC_LAYOUT_NEEDED(field, field.GetId(), self._on_expando_needs_layout)
1730 wx_expando.EVT_ETC_LAYOUT_NEEDED(self._TCTRL_episode_summary, self._TCTRL_episode_summary.GetId(), self._on_expando_needs_layout)
1731 gmDispatcher.connect(signal = u'doc_page_mod_db', receiver = self._refresh_visual_soap)
1732
1735
1737
1738
1739
1740
1741 self.FitInside()
1742
1743 if self.HasScrollbar(wx.VERTICAL):
1744
1745 expando = self.FindWindowById(evt.GetId())
1746 y_expando = expando.GetPositionTuple()[1]
1747 h_expando = expando.GetSizeTuple()[1]
1748 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1
1749 if expando.NumberOfLines == 0:
1750 no_of_lines = 1
1751 else:
1752 no_of_lines = expando.NumberOfLines
1753 y_cursor = int(round((float(line_cursor) / no_of_lines) * h_expando))
1754 y_desired_visible = y_expando + y_cursor
1755
1756 y_view = self.ViewStart[1]
1757 h_view = self.GetClientSizeTuple()[1]
1758
1759
1760
1761
1762
1763
1764
1765
1766 if y_desired_visible < y_view:
1767
1768 self.Scroll(0, y_desired_visible)
1769
1770 if y_desired_visible > h_view:
1771
1772 self.Scroll(0, y_desired_visible)
1773
1774
1775
1777 soap_notes = []
1778
1779 tmp = self._TCTRL_Soap.GetValue().strip()
1780 if tmp != u'':
1781 soap_notes.append(['s', tmp])
1782
1783 tmp = self._TCTRL_sOap.GetValue().strip()
1784 if tmp != u'':
1785 soap_notes.append(['o', tmp])
1786
1787 tmp = self._TCTRL_soAp.GetValue().strip()
1788 if tmp != u'':
1789 soap_notes.append(['a', tmp])
1790
1791 tmp = self._TCTRL_soaP.GetValue().strip()
1792 if tmp != u'':
1793 soap_notes.append(['p', tmp])
1794
1795 return soap_notes
1796
1797 soap = property(_get_soap, lambda x:x)
1798
1800
1801
1802 for field in self.soap_fields:
1803 if field.GetValue().strip() != u'':
1804 return False
1805
1806
1807 summary = self._TCTRL_episode_summary.GetValue().strip()
1808 if self.problem is None:
1809 if summary != u'':
1810 return False
1811 elif self.problem['type'] == u'issue':
1812 if summary != u'':
1813 return False
1814 else:
1815 if self.problem['summary'] is None:
1816 if summary != u'':
1817 return False
1818 else:
1819 if summary != self.problem['summary'].strip():
1820 return False
1821
1822
1823 new_codes = self._PRW_episode_codes.GetData()
1824 if self.problem is None:
1825 if len(new_codes) > 0:
1826 return False
1827 elif self.problem['type'] == u'issue':
1828 if len(new_codes) > 0:
1829 return False
1830 else:
1831 old_code_pks = self.problem.generic_codes
1832 if len(old_code_pks) != len(new_codes):
1833 return False
1834 for code in new_codes:
1835 if code['data'] not in old_code_pks:
1836 return False
1837
1838 return True
1839
1840 empty = property(_get_empty, lambda x:x)
1841
1842 -class cSoapLineTextCtrl(wx_expando.ExpandoTextCtrl):
1843
1844 - def __init__(self, *args, **kwargs):
1845
1846 wx_expando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
1847
1848 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+")
1849
1850 self.__register_interests()
1851
1852
1853
1854 - def _wrapLine(self, line, dc, width):
1855
1856 if (wx.MAJOR_VERSION >= 2) and (wx.MINOR_VERSION > 8):
1857 return super(cSoapLineTextCtrl, self)._wrapLine(line, dc, width)
1858
1859
1860
1861
1862 pte = dc.GetPartialTextExtents(line)
1863 width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X)
1864 idx = 0
1865 start = 0
1866 count = 0
1867 spc = -1
1868 while idx < len(pte):
1869 if line[idx] == ' ':
1870 spc = idx
1871 if pte[idx] - start > width:
1872
1873 count += 1
1874
1875 if spc != -1:
1876 idx = spc + 1
1877 spc = -1
1878 if idx < len(pte):
1879 start = pte[idx]
1880 else:
1881 idx += 1
1882 return count
1883
1884
1885
1887
1888
1889 wx.EVT_CHAR(self, self.__on_char)
1890 wx.EVT_SET_FOCUS(self, self.__on_focus)
1891
1892 - def __on_focus(self, evt):
1893 evt.Skip()
1894 wx.CallAfter(self._after_on_focus)
1895
1896 - def _after_on_focus(self):
1897
1898 evt = wx.PyCommandEvent(wx_expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
1899 evt.SetEventObject(self)
1900
1901
1902
1903
1904 self.GetEventHandler().ProcessEvent(evt)
1905
1906 - def __on_char(self, evt):
1907 char = unichr(evt.GetUnicodeKey())
1908
1909 if self.LastPosition == 1:
1910 evt.Skip()
1911 return
1912
1913 explicit_expansion = False
1914 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT):
1915 if evt.GetKeyCode() != 13:
1916 evt.Skip()
1917 return
1918 explicit_expansion = True
1919
1920 if not explicit_expansion:
1921 if self.__keyword_separators.match(char) is None:
1922 evt.Skip()
1923 return
1924
1925 caret_pos, line_no = self.PositionToXY(self.InsertionPoint)
1926 line = self.GetLineText(line_no)
1927 keyword = self.__keyword_separators.split(line[:caret_pos])[-1]
1928
1929 if (
1930 (not explicit_expansion)
1931 and
1932 (keyword != u'$$steffi')
1933 and
1934 (keyword not in [ r[0] for r in gmPG2.get_text_expansion_keywords() ])
1935 ):
1936 evt.Skip()
1937 return
1938
1939 start = self.InsertionPoint - len(keyword)
1940 wx.CallAfter(self.replace_keyword_with_expansion, keyword, start, explicit_expansion)
1941
1942 evt.Skip()
1943 return
1944
1945 - def replace_keyword_with_expansion(self, keyword=None, position=None, show_list=False):
1946
1947 expansion = gmTextExpansionWidgets.expand_keyword(parent = self, keyword = keyword, show_list = show_list)
1948
1949 if expansion is None:
1950 return
1951
1952 if expansion == u'':
1953 return
1954
1955 self.Replace (
1956 position,
1957 position + len(keyword),
1958 expansion
1959 )
1960
1961 self.SetInsertionPoint(position + len(expansion) + 1)
1962 self.ShowPosition(position + len(expansion) + 1)
1963
1964 return
1965
1966
1967
1998
1999 cmd = gmCfgWidgets.configure_string_option (
2000 message = _(
2001 'Enter the shell command with which to start\n'
2002 'the image editor for visual progress notes.\n'
2003 '\n'
2004 'Any "%(img)s" included with the arguments\n'
2005 'will be replaced by the file name of the\n'
2006 'note template.'
2007 ),
2008 option = u'external.tools.visual_soap_editor_cmd',
2009 bias = 'user',
2010 default_value = None,
2011 validator = is_valid
2012 )
2013
2014 return cmd
2015
2017 if parent is None:
2018 parent = wx.GetApp().GetTopWindow()
2019
2020 dlg = wx.FileDialog (
2021 parent = parent,
2022 message = _('Choose file to use as template for new visual progress note'),
2023 defaultDir = os.path.expanduser('~'),
2024 defaultFile = '',
2025
2026 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST
2027 )
2028 result = dlg.ShowModal()
2029
2030 if result == wx.ID_CANCEL:
2031 dlg.Destroy()
2032 return None
2033
2034 full_filename = dlg.GetPath()
2035 dlg.Hide()
2036 dlg.Destroy()
2037 return full_filename
2038
2040
2041 if parent is None:
2042 parent = wx.GetApp().GetTopWindow()
2043
2044 dlg = gmGuiHelpers.c3ButtonQuestionDlg (
2045 parent,
2046 -1,
2047 caption = _('Visual progress note source'),
2048 question = _('From which source do you want to pick the image template ?'),
2049 button_defs = [
2050 {'label': _('Database'), 'tooltip': _('List of templates in the database.'), 'default': True},
2051 {'label': _('File'), 'tooltip': _('Files in the filesystem.'), 'default': False},
2052 {'label': _('Device'), 'tooltip': _('Image capture devices (scanners, cameras, etc)'), 'default': False}
2053 ]
2054 )
2055 result = dlg.ShowModal()
2056 dlg.Destroy()
2057
2058
2059 if result == wx.ID_YES:
2060 _log.debug('visual progress note template from: database template')
2061 from Gnumed.wxpython import gmFormWidgets
2062 template = gmFormWidgets.manage_form_templates (
2063 parent = parent,
2064 template_types = [gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE],
2065 active_only = True
2066 )
2067 if template is None:
2068 return (None, None)
2069 filename = template.export_to_file()
2070 if filename is None:
2071 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note template for [%s].') % template['name_long'])
2072 return (None, None)
2073 return (filename, True)
2074
2075
2076 if result == wx.ID_NO:
2077 _log.debug('visual progress note template from: disk file')
2078 fname = select_file_as_visual_progress_note_template(parent = parent)
2079 if fname is None:
2080 return (None, None)
2081
2082 ext = os.path.splitext(fname)[1]
2083 tmp_name = gmTools.get_unique_filename(suffix = ext)
2084 _log.debug('visual progress note from file: [%s] -> [%s]', fname, tmp_name)
2085 shutil.copy2(fname, tmp_name)
2086 return (tmp_name, False)
2087
2088
2089 if result == wx.ID_CANCEL:
2090 _log.debug('visual progress note template from: image capture device')
2091 fnames = gmDocumentWidgets.acquire_images_from_capture_device(device = None, calling_window = parent)
2092 if fnames is None:
2093 return (None, None)
2094 if len(fnames) == 0:
2095 return (None, None)
2096 return (fnames[0], False)
2097
2098 _log.debug('no visual progress note template source selected')
2099 return (None, None)
2100
2102 """This assumes <filename> contains an image which can be handled by the configured image editor."""
2103
2104 if doc_part is not None:
2105 filename = doc_part.export_to_file()
2106 if filename is None:
2107 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.'))
2108 return None
2109
2110 dbcfg = gmCfg.cCfgSQL()
2111 cmd = dbcfg.get2 (
2112 option = u'external.tools.visual_soap_editor_cmd',
2113 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2114 bias = 'user'
2115 )
2116
2117 if cmd is None:
2118 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = False)
2119 cmd = configure_visual_progress_note_editor()
2120 if cmd is None:
2121 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = True)
2122 return None
2123
2124 if u'%(img)s' in cmd:
2125 cmd = cmd % {u'img': filename}
2126 else:
2127 cmd = u'%s %s' % (cmd, filename)
2128
2129 if discard_unmodified:
2130 original_stat = os.stat(filename)
2131 original_md5 = gmTools.file2md5(filename)
2132
2133 success = gmShellAPI.run_command_in_shell(cmd, blocking = True)
2134 if not success:
2135 gmGuiHelpers.gm_show_error (
2136 _(
2137 'There was a problem with running the editor\n'
2138 'for visual progress notes.\n'
2139 '\n'
2140 ' [%s]\n'
2141 '\n'
2142 ) % cmd,
2143 _('Editing visual progress note')
2144 )
2145 return None
2146
2147 try:
2148 open(filename, 'r').close()
2149 except StandardError:
2150 _log.exception('problem accessing visual progress note file [%s]', filename)
2151 gmGuiHelpers.gm_show_error (
2152 _(
2153 'There was a problem reading the visual\n'
2154 'progress note from the file:\n'
2155 '\n'
2156 ' [%s]\n'
2157 '\n'
2158 ) % filename,
2159 _('Saving visual progress note')
2160 )
2161 return None
2162
2163 if discard_unmodified:
2164 modified_stat = os.stat(filename)
2165
2166 if original_stat.st_size == modified_stat.st_size:
2167 modified_md5 = gmTools.file2md5(filename)
2168
2169 if original_md5 == modified_md5:
2170 _log.debug('visual progress note (template) not modified')
2171
2172 msg = _(
2173 u'You either created a visual progress note from a template\n'
2174 u'in the database (rather than from a file on disk) or you\n'
2175 u'edited an existing visual progress note.\n'
2176 u'\n'
2177 u'The template/original was not modified at all, however.\n'
2178 u'\n'
2179 u'Do you still want to save the unmodified image as a\n'
2180 u'visual progress note into the EMR of the patient ?\n'
2181 )
2182 save_unmodified = gmGuiHelpers.gm_show_question (
2183 msg,
2184 _('Saving visual progress note')
2185 )
2186 if not save_unmodified:
2187 _log.debug('user discarded unmodified note')
2188 return
2189
2190 if doc_part is not None:
2191 _log.debug('updating visual progress note')
2192 doc_part.update_data_from_file(fname = filename)
2193 doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2194 return None
2195
2196 if not isinstance(episode, gmEMRStructItems.cEpisode):
2197 if episode is None:
2198 episode = _('visual progress notes')
2199 pat = gmPerson.gmCurrentPatient()
2200 emr = pat.get_emr()
2201 episode = emr.add_episode(episode_name = episode.strip(), pk_health_issue = health_issue, is_open = False)
2202
2203 doc = gmDocumentWidgets.save_file_as_new_document (
2204 filename = filename,
2205 document_type = gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE,
2206 episode = episode,
2207 unlock_patient = False
2208 )
2209 doc.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2210
2211 return doc
2212
2214 """Phrasewheel to allow selection of visual SOAP template."""
2215
2217
2218 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
2219
2220 query = u"""
2221 SELECT
2222 pk AS data,
2223 name_short AS list_label,
2224 name_sort AS field_label
2225 FROM
2226 ref.paperwork_templates
2227 WHERE
2228 fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND (
2229 name_long %%(fragment_condition)s
2230 OR
2231 name_short %%(fragment_condition)s
2232 )
2233 ORDER BY list_label
2234 LIMIT 15
2235 """ % gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE
2236
2237 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
2238 mp.setThresholds(2, 3, 5)
2239
2240 self.matcher = mp
2241 self.selection_only = True
2242
2248
2249 from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl
2250
2252
2257
2258
2259
2260 - def refresh(self, document_folder=None, episodes=None, encounter=None):
2261
2262 self.clear()
2263 if document_folder is not None:
2264 soap_docs = document_folder.get_visual_progress_notes(episodes = episodes, encounter = encounter)
2265 if len(soap_docs) > 0:
2266 for soap_doc in soap_docs:
2267 parts = soap_doc.parts
2268 if len(parts) == 0:
2269 continue
2270 part = parts[0]
2271 fname = part.export_to_file()
2272 if fname is None:
2273 continue
2274
2275
2276 img = gmGuiHelpers.file2scaled_image (
2277 filename = fname,
2278 height = 30
2279 )
2280
2281 bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER)
2282
2283
2284 img = gmGuiHelpers.file2scaled_image (
2285 filename = fname,
2286 height = 150
2287 )
2288 tip = agw_stt.SuperToolTip (
2289 u'',
2290 bodyImage = img,
2291 header = _('Created: %s') % part['date_generated'].strftime('%Y %B %d').decode(gmI18N.get_encoding()),
2292 footer = gmTools.coalesce(part['doc_comment'], u'').strip()
2293 )
2294 tip.SetTopGradientColor('white')
2295 tip.SetMiddleGradientColor('white')
2296 tip.SetBottomGradientColor('white')
2297 tip.SetTarget(bmp)
2298
2299 bmp.doc_part = part
2300 bmp.Bind(wx.EVT_LEFT_UP, self._on_bitmap_leftclicked)
2301
2302 self._SZR_soap.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM | wx.EXPAND, 3)
2303 self.__bitmaps.append(bmp)
2304
2305 self.GetParent().Layout()
2306
2308 while len(self._SZR_soap.GetChildren()) > 0:
2309 self._SZR_soap.Detach(0)
2310
2311
2312 for bmp in self.__bitmaps:
2313 bmp.Destroy()
2314 self.__bitmaps = []
2315
2317 wx.CallAfter (
2318 edit_visual_progress_note,
2319 doc_part = evt.GetEventObject().doc_part,
2320 discard_unmodified = True
2321 )
2322
2323 from Gnumed.wxGladeWidgets import wxgSimpleSoapPluginPnl
2324
2325 -class cSimpleSoapPluginPnl(wxgSimpleSoapPluginPnl.wxgSimpleSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
2335
2336
2337
2339 self._LCTRL_problems.set_columns(columns = [_('Problem list')])
2340 self._LCTRL_problems.activate_callback = self._on_problem_activated
2341 self._LCTRL_problems.item_tooltip_callback = self._on_get_problem_tooltip
2342
2343 self._splitter_main.SetSashGravity(0.5)
2344 splitter_width = self._splitter_main.GetSizeTuple()[0]
2345 self._splitter_main.SetSashPosition(splitter_width / 2, True)
2346
2347 self._TCTRL_soap.Disable()
2348 self._BTN_save_soap.Disable()
2349 self._BTN_clear_soap.Disable()
2350
2352 self._LCTRL_problems.set_string_items()
2353 self._TCTRL_soap_problem.SetValue(_('<above, double-click problem to start entering SOAP note>'))
2354 self._TCTRL_soap.SetValue(u'')
2355 self._CHBOX_filter_by_problem.SetLabel(_('&Filter by problem'))
2356 self._TCTRL_journal.SetValue(u'')
2357
2358 self._TCTRL_soap.Disable()
2359 self._BTN_save_soap.Disable()
2360 self._BTN_clear_soap.Disable()
2361
2363 if not self.__curr_pat.connected:
2364 return None
2365
2366 if self.__problem is None:
2367 return None
2368
2369 saved = self.__curr_pat.emr.add_clin_narrative (
2370 note = self._TCTRL_soap.GetValue().strip(),
2371 soap_cat = u'u',
2372 episode = self.__problem
2373 )
2374
2375 if saved is None:
2376 return False
2377
2378 self._TCTRL_soap.SetValue(u'')
2379 self.__refresh_journal()
2380 return True
2381
2383 if self._TCTRL_soap.GetValue().strip() == u'':
2384 return True
2385 if self.__problem is None:
2386
2387 self._TCTRL_soap.SetValue(u'')
2388 return None
2389 save_it = gmGuiHelpers.gm_show_question (
2390 title = _('Saving SOAP note'),
2391 question = _('Do you want to save the SOAP note ?')
2392 )
2393 if save_it:
2394 return self.__save_soap()
2395 return False
2396
2407
2428
2429
2430
2432 """Configure enabled event signals."""
2433
2434 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
2435 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
2436 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db)
2437 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
2438
2439
2440 self.__curr_pat.register_pre_selection_callback(callback = self._pre_selection_callback)
2441 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
2442
2444 """Another patient is about to be activated.
2445
2446 Patient change will not proceed before this returns True.
2447 """
2448 if not self.__curr_pat.connected:
2449 return True
2450 self.__perhaps_save_soap()
2451 self.__problem = None
2452 return True
2453
2455 """The client is about to be shut down.
2456
2457 Shutdown will not proceed before this returns.
2458 """
2459 if not self.__curr_pat.connected:
2460 return
2461 if not self.__save_soap():
2462 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save SimpleNotes SOAP note.'), beep = True)
2463 return
2464
2466 wx.CallAfter(self.__reset_ui)
2467
2469 wx.CallAfter(self._schedule_data_reget)
2470
2472 wx.CallAfter(self._schedule_data_reget)
2473
2475 self.__perhaps_save_soap()
2476 epi = self._LCTRL_problems.get_selected_item_data(only_one = True)
2477 self._TCTRL_soap_problem.SetValue(_('Progress note: %s%s') % (
2478 epi['description'],
2479 gmTools.coalesce(epi['health_issue'], u'', u' (%s)')
2480 ))
2481 self.__problem = epi
2482 self._TCTRL_soap.SetValue(u'')
2483
2484 self._TCTRL_soap.Enable()
2485 self._BTN_save_soap.Enable()
2486 self._BTN_clear_soap.Enable()
2487
2502
2504 event.Skip()
2505 self.__refresh_journal()
2506
2508 event.Skip()
2509 self.__refresh_journal()
2510
2525
2532
2540
2544
2548
2549
2550
2552 self.__refresh_problem_list()
2553 self.__refresh_journal()
2554 self._TCTRL_soap.SetValue(u'')
2555 return True
2556
2557
2558
2559
2560 if __name__ == '__main__':
2561
2562 if len(sys.argv) < 2:
2563 sys.exit()
2564
2565 if sys.argv[1] != 'test':
2566 sys.exit()
2567
2568 gmI18N.activate_locale()
2569 gmI18N.install_domain(domain = 'gnumed')
2570
2571
2580
2587
2600
2601
2602 test_cSoapNoteExpandoEditAreaPnl()
2603
2604
2605
2606