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 gmEMRStructItems
28 from Gnumed.business import gmClinNarrative
29 from Gnumed.business import gmSurgery
30 from Gnumed.business import gmForms
31 from Gnumed.business import gmDocuments
32 from Gnumed.business import gmPersonSearch
33
34 from Gnumed.wxpython import gmListWidgets
35 from Gnumed.wxpython import gmEMRStructWidgets
36 from Gnumed.wxpython import gmRegetMixin
37 from Gnumed.wxpython import gmPhraseWheel
38 from Gnumed.wxpython import gmGuiHelpers
39 from Gnumed.wxpython import gmPatSearchWidgets
40 from Gnumed.wxpython import gmCfgWidgets
41 from Gnumed.wxpython import gmDocumentWidgets
42 from Gnumed.wxpython import gmTextExpansionWidgets
43
44 from Gnumed.exporters import gmPatientExporter
45
46
47 _log = logging.getLogger('gm.ui')
48 _log.info(__version__)
49
50
51
53
54
55 if patient is None:
56 patient = gmPerson.gmCurrentPatient()
57
58 if not patient.connected:
59 gmDispatcher.send(signal = 'statustext', msg = _('Cannot move progress notes. No active patient.'))
60 return False
61
62 if parent is None:
63 parent = wx.GetApp().GetTopWindow()
64
65 emr = patient.get_emr()
66
67 if encounters is None:
68 encs = emr.get_encounters(episodes = episodes)
69 encounters = gmEMRStructWidgets.select_encounters (
70 parent = parent,
71 patient = patient,
72 single_selection = False,
73 encounters = encs
74 )
75
76 if encounters is None:
77 return True
78
79 if len(encounters) == 0:
80 return True
81
82 notes = emr.get_clin_narrative (
83 encounters = encounters,
84 episodes = episodes
85 )
86
87
88 if move_all:
89 selected_narr = notes
90 else:
91 selected_narr = gmListWidgets.get_choices_from_list (
92 parent = parent,
93 caption = _('Moving progress notes between encounters ...'),
94 single_selection = False,
95 can_return_empty = True,
96 data = notes,
97 msg = _('\n Select the progress notes to move from the list !\n\n'),
98 columns = [_('when'), _('who'), _('type'), _('entry')],
99 choices = [
100 [ narr['date'].strftime('%x %H:%M'),
101 narr['provider'],
102 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
103 narr['narrative'].replace('\n', '/').replace('\r', '/')
104 ] for narr in notes
105 ]
106 )
107
108 if not selected_narr:
109 return True
110
111
112 enc2move2 = gmEMRStructWidgets.select_encounters (
113 parent = parent,
114 patient = patient,
115 single_selection = True
116 )
117
118 if not enc2move2:
119 return True
120
121 for narr in selected_narr:
122 narr['pk_encounter'] = enc2move2['pk_encounter']
123 narr.save()
124
125 return True
126
128
129
130 if patient is None:
131 patient = gmPerson.gmCurrentPatient()
132
133 if not patient.connected:
134 gmDispatcher.send(signal = 'statustext', msg = _('Cannot edit progress notes. No active patient.'))
135 return False
136
137 if parent is None:
138 parent = wx.GetApp().GetTopWindow()
139
140 emr = patient.get_emr()
141
142 def delete(item):
143 if item is None:
144 return False
145 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
146 parent,
147 -1,
148 caption = _('Deleting progress note'),
149 question = _(
150 'Are you positively sure you want to delete this\n'
151 'progress note from the medical record ?\n'
152 '\n'
153 'Note that even if you chose to delete the entry it will\n'
154 'still be (invisibly) kept in the audit trail to protect\n'
155 'you from litigation because physical deletion is known\n'
156 'to be unlawful in some jurisdictions.\n'
157 ),
158 button_defs = (
159 {'label': _('Delete'), 'tooltip': _('Yes, delete the progress note.'), 'default': False},
160 {'label': _('Cancel'), 'tooltip': _('No, do NOT delete the progress note.'), 'default': True}
161 )
162 )
163 decision = dlg.ShowModal()
164
165 if decision != wx.ID_YES:
166 return False
167
168 gmClinNarrative.delete_clin_narrative(narrative = item['pk_narrative'])
169 return True
170
171 def edit(item):
172 if item is None:
173 return False
174
175 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
176 parent,
177 -1,
178 title = _('Editing progress note'),
179 msg = _('This is the original progress note:'),
180 data = item.format(left_margin = u' ', fancy = True),
181 text = item['narrative']
182 )
183 decision = dlg.ShowModal()
184
185 if decision != wx.ID_SAVE:
186 return False
187
188 val = dlg.value
189 dlg.Destroy()
190 if val.strip() == u'':
191 return False
192
193 item['narrative'] = val
194 item.save_payload()
195
196 return True
197
198 def refresh(lctrl):
199 notes = emr.get_clin_narrative (
200 encounters = encounters,
201 episodes = episodes,
202 providers = [ gmStaff.gmCurrentProvider()['short_alias'] ]
203 )
204 lctrl.set_string_items(items = [
205 [ narr['date'].strftime('%x %H:%M'),
206 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
207 narr['narrative'].replace('\n', '/').replace('\r', '/')
208 ] for narr in notes
209 ])
210 lctrl.set_data(data = notes)
211
212
213 gmListWidgets.get_choices_from_list (
214 parent = parent,
215 caption = _('Managing progress notes'),
216 msg = _(
217 '\n'
218 ' This list shows the progress notes by %s.\n'
219 '\n'
220 ) % gmStaff.gmCurrentProvider()['short_alias'],
221 columns = [_('when'), _('type'), _('entry')],
222 single_selection = True,
223 can_return_empty = False,
224 edit_callback = edit,
225 delete_callback = delete,
226 refresh_callback = refresh,
227 ignore_OK_button = True
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
1733
1734
1735
1736
1737 self.FitInside()
1738
1739 if self.HasScrollbar(wx.VERTICAL):
1740
1741 expando = self.FindWindowById(evt.GetId())
1742 y_expando = expando.GetPositionTuple()[1]
1743 h_expando = expando.GetSizeTuple()[1]
1744 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1
1745 if expando.NumberOfLines == 0:
1746 no_of_lines = 1
1747 else:
1748 no_of_lines = expando.NumberOfLines
1749 y_cursor = int(round((float(line_cursor) / no_of_lines) * h_expando))
1750 y_desired_visible = y_expando + y_cursor
1751
1752 y_view = self.ViewStart[1]
1753 h_view = self.GetClientSizeTuple()[1]
1754
1755
1756
1757
1758
1759
1760
1761
1762 if y_desired_visible < y_view:
1763
1764 self.Scroll(0, y_desired_visible)
1765
1766 if y_desired_visible > h_view:
1767
1768 self.Scroll(0, y_desired_visible)
1769
1770
1771
1773 soap_notes = []
1774
1775 tmp = self._TCTRL_Soap.GetValue().strip()
1776 if tmp != u'':
1777 soap_notes.append(['s', tmp])
1778
1779 tmp = self._TCTRL_sOap.GetValue().strip()
1780 if tmp != u'':
1781 soap_notes.append(['o', tmp])
1782
1783 tmp = self._TCTRL_soAp.GetValue().strip()
1784 if tmp != u'':
1785 soap_notes.append(['a', tmp])
1786
1787 tmp = self._TCTRL_soaP.GetValue().strip()
1788 if tmp != u'':
1789 soap_notes.append(['p', tmp])
1790
1791 return soap_notes
1792
1793 soap = property(_get_soap, lambda x:x)
1794
1796
1797
1798 for field in self.soap_fields:
1799 if field.GetValue().strip() != u'':
1800 return False
1801
1802
1803 summary = self._TCTRL_episode_summary.GetValue().strip()
1804 if self.problem is None:
1805 if summary != u'':
1806 return False
1807 elif self.problem['type'] == u'issue':
1808 if summary != u'':
1809 return False
1810 else:
1811 if self.problem['summary'] is None:
1812 if summary != u'':
1813 return False
1814 else:
1815 if summary != self.problem['summary'].strip():
1816 return False
1817
1818
1819 new_codes = self._PRW_episode_codes.GetData()
1820 if self.problem is None:
1821 if len(new_codes) > 0:
1822 return False
1823 elif self.problem['type'] == u'issue':
1824 if len(new_codes) > 0:
1825 return False
1826 else:
1827 old_code_pks = self.problem.generic_codes
1828 if len(old_code_pks) != len(new_codes):
1829 return False
1830 for code in new_codes:
1831 if code['data'] not in old_code_pks:
1832 return False
1833
1834 return True
1835
1836 empty = property(_get_empty, lambda x:x)
1837
1838 -class cSoapLineTextCtrl(wx_expando.ExpandoTextCtrl):
1839
1840 - def __init__(self, *args, **kwargs):
1841
1842 wx_expando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
1843
1844 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+")
1845
1846 self.__register_interests()
1847
1848
1849
1850 - def _wrapLine(self, line, dc, width):
1851
1852 if (wx.MAJOR_VERSION >= 2) and (wx.MINOR_VERSION > 8):
1853 return super(cSoapLineTextCtrl, self)._wrapLine(line, dc, width)
1854
1855
1856
1857
1858 pte = dc.GetPartialTextExtents(line)
1859 width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X)
1860 idx = 0
1861 start = 0
1862 count = 0
1863 spc = -1
1864 while idx < len(pte):
1865 if line[idx] == ' ':
1866 spc = idx
1867 if pte[idx] - start > width:
1868
1869 count += 1
1870
1871 if spc != -1:
1872 idx = spc + 1
1873 spc = -1
1874 if idx < len(pte):
1875 start = pte[idx]
1876 else:
1877 idx += 1
1878 return count
1879
1880
1881
1883
1884
1885 wx.EVT_CHAR(self, self.__on_char)
1886 wx.EVT_SET_FOCUS(self, self.__on_focus)
1887
1888 - def __on_focus(self, evt):
1889 evt.Skip()
1890 wx.CallAfter(self._after_on_focus)
1891
1892 - def _after_on_focus(self):
1893
1894 evt = wx.PyCommandEvent(wx_expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
1895 evt.SetEventObject(self)
1896
1897
1898
1899
1900 self.GetEventHandler().ProcessEvent(evt)
1901
1902 - def __on_char(self, evt):
1903 char = unichr(evt.GetUnicodeKey())
1904
1905 if self.LastPosition == 1:
1906 evt.Skip()
1907 return
1908
1909 explicit_expansion = False
1910 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT):
1911 if evt.GetKeyCode() != 13:
1912 evt.Skip()
1913 return
1914 explicit_expansion = True
1915
1916 if not explicit_expansion:
1917 if self.__keyword_separators.match(char) is None:
1918 evt.Skip()
1919 return
1920
1921 caret_pos, line_no = self.PositionToXY(self.InsertionPoint)
1922 line = self.GetLineText(line_no)
1923 keyword = self.__keyword_separators.split(line[:caret_pos])[-1]
1924
1925 if (
1926 (not explicit_expansion)
1927 and
1928 (keyword != u'$$steffi')
1929 and
1930 (keyword not in [ r[0] for r in gmPG2.get_text_expansion_keywords() ])
1931 ):
1932 evt.Skip()
1933 return
1934
1935 start = self.InsertionPoint - len(keyword)
1936 wx.CallAfter(self.replace_keyword_with_expansion, keyword, start, explicit_expansion)
1937
1938 evt.Skip()
1939 return
1940
1941 - def replace_keyword_with_expansion(self, keyword=None, position=None, show_list=False):
1942
1943 expansion = gmTextExpansionWidgets.expand_keyword(parent = self, keyword = keyword, show_list = show_list)
1944
1945 if expansion is None:
1946 return
1947
1948 if expansion == u'':
1949 return
1950
1951 self.Replace (
1952 position,
1953 position + len(keyword),
1954 expansion
1955 )
1956
1957 self.SetInsertionPoint(position + len(expansion) + 1)
1958 self.ShowPosition(position + len(expansion) + 1)
1959
1960 return
1961
1962
1963
1994
1995 cmd = gmCfgWidgets.configure_string_option (
1996 message = _(
1997 'Enter the shell command with which to start\n'
1998 'the image editor for visual progress notes.\n'
1999 '\n'
2000 'Any "%(img)s" included with the arguments\n'
2001 'will be replaced by the file name of the\n'
2002 'note template.'
2003 ),
2004 option = u'external.tools.visual_soap_editor_cmd',
2005 bias = 'user',
2006 default_value = None,
2007 validator = is_valid
2008 )
2009
2010 return cmd
2011
2013 if parent is None:
2014 parent = wx.GetApp().GetTopWindow()
2015
2016 dlg = wx.FileDialog (
2017 parent = parent,
2018 message = _('Choose file to use as template for new visual progress note'),
2019 defaultDir = os.path.expanduser('~'),
2020 defaultFile = '',
2021
2022 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST
2023 )
2024 result = dlg.ShowModal()
2025
2026 if result == wx.ID_CANCEL:
2027 dlg.Destroy()
2028 return None
2029
2030 full_filename = dlg.GetPath()
2031 dlg.Hide()
2032 dlg.Destroy()
2033 return full_filename
2034
2036
2037 if parent is None:
2038 parent = wx.GetApp().GetTopWindow()
2039
2040 dlg = gmGuiHelpers.c3ButtonQuestionDlg (
2041 parent,
2042 -1,
2043 caption = _('Visual progress note source'),
2044 question = _('From which source do you want to pick the image template ?'),
2045 button_defs = [
2046 {'label': _('Database'), 'tooltip': _('List of templates in the database.'), 'default': True},
2047 {'label': _('File'), 'tooltip': _('Files in the filesystem.'), 'default': False},
2048 {'label': _('Device'), 'tooltip': _('Image capture devices (scanners, cameras, etc)'), 'default': False}
2049 ]
2050 )
2051 result = dlg.ShowModal()
2052 dlg.Destroy()
2053
2054
2055 if result == wx.ID_YES:
2056 _log.debug('visual progress note template from: database template')
2057 from Gnumed.wxpython import gmFormWidgets
2058 template = gmFormWidgets.manage_form_templates (
2059 parent = parent,
2060 template_types = [gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE],
2061 active_only = True
2062 )
2063 if template is None:
2064 return (None, None)
2065 filename = template.export_to_file()
2066 if filename is None:
2067 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note template for [%s].') % template['name_long'])
2068 return (None, None)
2069 return (filename, True)
2070
2071
2072 if result == wx.ID_NO:
2073 _log.debug('visual progress note template from: disk file')
2074 fname = select_file_as_visual_progress_note_template(parent = parent)
2075 if fname is None:
2076 return (None, None)
2077
2078 ext = os.path.splitext(fname)[1]
2079 tmp_name = gmTools.get_unique_filename(suffix = ext)
2080 _log.debug('visual progress note from file: [%s] -> [%s]', fname, tmp_name)
2081 shutil.copy2(fname, tmp_name)
2082 return (tmp_name, False)
2083
2084
2085 if result == wx.ID_CANCEL:
2086 _log.debug('visual progress note template from: image capture device')
2087 fnames = gmDocumentWidgets.acquire_images_from_capture_device(device = None, calling_window = parent)
2088 if fnames is None:
2089 return (None, None)
2090 if len(fnames) == 0:
2091 return (None, None)
2092 return (fnames[0], False)
2093
2094 _log.debug('no visual progress note template source selected')
2095 return (None, None)
2096
2098 """This assumes <filename> contains an image which can be handled by the configured image editor."""
2099
2100 if doc_part is not None:
2101 filename = doc_part.export_to_file()
2102 if filename is None:
2103 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.'))
2104 return None
2105
2106 dbcfg = gmCfg.cCfgSQL()
2107 cmd = dbcfg.get2 (
2108 option = u'external.tools.visual_soap_editor_cmd',
2109 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2110 bias = 'user'
2111 )
2112
2113 if cmd is None:
2114 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = False)
2115 cmd = configure_visual_progress_note_editor()
2116 if cmd is None:
2117 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = True)
2118 return None
2119
2120 if u'%(img)s' in cmd:
2121 cmd % {u'img': filename}
2122 else:
2123 cmd = u'%s %s' % (cmd, filename)
2124
2125 if discard_unmodified:
2126 original_stat = os.stat(filename)
2127 original_md5 = gmTools.file2md5(filename)
2128
2129 success = gmShellAPI.run_command_in_shell(cmd, blocking = True)
2130 if not success:
2131 gmGuiHelpers.gm_show_error (
2132 _(
2133 'There was a problem with running the editor\n'
2134 'for visual progress notes.\n'
2135 '\n'
2136 ' [%s]\n'
2137 '\n'
2138 ) % cmd,
2139 _('Editing visual progress note')
2140 )
2141 return None
2142
2143 try:
2144 open(filename, 'r').close()
2145 except StandardError:
2146 _log.exception('problem accessing visual progress note file [%s]', filename)
2147 gmGuiHelpers.gm_show_error (
2148 _(
2149 'There was a problem reading the visual\n'
2150 'progress note from the file:\n'
2151 '\n'
2152 ' [%s]\n'
2153 '\n'
2154 ) % filename,
2155 _('Saving visual progress note')
2156 )
2157 return None
2158
2159 if discard_unmodified:
2160 modified_stat = os.stat(filename)
2161
2162 if original_stat.st_size == modified_stat.st_size:
2163 modified_md5 = gmTools.file2md5(filename)
2164
2165 if original_md5 == modified_md5:
2166 _log.debug('visual progress note (template) not modified')
2167
2168 msg = _(
2169 u'You either created a visual progress note from a template\n'
2170 u'in the database (rather than from a file on disk) or you\n'
2171 u'edited an existing visual progress note.\n'
2172 u'\n'
2173 u'The template/original was not modified at all, however.\n'
2174 u'\n'
2175 u'Do you still want to save the unmodified image as a\n'
2176 u'visual progress note into the EMR of the patient ?\n'
2177 )
2178 save_unmodified = gmGuiHelpers.gm_show_question (
2179 msg,
2180 _('Saving visual progress note')
2181 )
2182 if not save_unmodified:
2183 _log.debug('user discarded unmodified note')
2184 return
2185
2186 if doc_part is not None:
2187 doc_part.update_data_from_file(fname = filename)
2188 doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2189 return None
2190
2191 if not isinstance(episode, gmEMRStructItems.cEpisode):
2192 if episode is None:
2193 episode = _('visual progress notes')
2194 pat = gmPerson.gmCurrentPatient()
2195 emr = pat.get_emr()
2196 episode = emr.add_episode(episode_name = episode.strip(), pk_health_issue = health_issue, is_open = False)
2197
2198 doc = gmDocumentWidgets.save_file_as_new_document (
2199 filename = filename,
2200 document_type = gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE,
2201 episode = episode,
2202 unlock_patient = True
2203 )
2204 doc.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2205
2206 return doc
2207
2209 """Phrasewheel to allow selection of visual SOAP template."""
2210
2212
2213 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
2214
2215 query = u"""
2216 SELECT
2217 pk AS data,
2218 name_short AS list_label,
2219 name_sort AS field_label
2220 FROM
2221 ref.paperwork_templates
2222 WHERE
2223 fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND (
2224 name_long %%(fragment_condition)s
2225 OR
2226 name_short %%(fragment_condition)s
2227 )
2228 ORDER BY list_label
2229 LIMIT 15
2230 """ % gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE
2231
2232 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
2233 mp.setThresholds(2, 3, 5)
2234
2235 self.matcher = mp
2236 self.selection_only = True
2237
2243
2244 from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl
2245
2247
2252
2253
2254
2255 - def refresh(self, document_folder=None, episodes=None, encounter=None):
2256
2257 self.clear()
2258 if document_folder is not None:
2259 soap_docs = document_folder.get_visual_progress_notes(episodes = episodes, encounter = encounter)
2260 if len(soap_docs) > 0:
2261 for soap_doc in soap_docs:
2262 parts = soap_doc.parts
2263 if len(parts) == 0:
2264 continue
2265 part = parts[0]
2266 fname = part.export_to_file()
2267 if fname is None:
2268 continue
2269
2270
2271 img = gmGuiHelpers.file2scaled_image (
2272 filename = fname,
2273 height = 30
2274 )
2275
2276 bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER)
2277
2278
2279 img = gmGuiHelpers.file2scaled_image (
2280 filename = fname,
2281 height = 150
2282 )
2283 tip = agw_stt.SuperToolTip (
2284 u'',
2285 bodyImage = img,
2286 header = _('Created: %s') % part['date_generated'].strftime('%Y %B %d').decode(gmI18N.get_encoding()),
2287 footer = gmTools.coalesce(part['doc_comment'], u'').strip()
2288 )
2289 tip.SetTopGradientColor('white')
2290 tip.SetMiddleGradientColor('white')
2291 tip.SetBottomGradientColor('white')
2292 tip.SetTarget(bmp)
2293
2294 bmp.doc_part = part
2295 bmp.Bind(wx.EVT_LEFT_UP, self._on_bitmap_leftclicked)
2296
2297 self._SZR_soap.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM | wx.EXPAND, 3)
2298 self.__bitmaps.append(bmp)
2299
2300 self.GetParent().Layout()
2301
2303 while len(self._SZR_soap.GetChildren()) > 0:
2304 self._SZR_soap.Detach(0)
2305
2306
2307 for bmp in self.__bitmaps:
2308 bmp.Destroy()
2309 self.__bitmaps = []
2310
2312 wx.CallAfter (
2313 edit_visual_progress_note,
2314 doc_part = evt.GetEventObject().doc_part,
2315 discard_unmodified = True
2316 )
2317
2318 from Gnumed.wxGladeWidgets import wxgSimpleSoapPluginPnl
2319
2320 -class cSimpleSoapPluginPnl(wxgSimpleSoapPluginPnl.wxgSimpleSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
2330
2331
2332
2334 self._LCTRL_problems.set_columns(columns = [_('Problem list')])
2335 self._LCTRL_problems.activate_callback = self._on_problem_activated
2336 self._LCTRL_problems.item_tooltip_callback = self._on_get_problem_tooltip
2337
2338 self._splitter_main.SetSashGravity(0.5)
2339 splitter_width = self._splitter_main.GetSizeTuple()[0]
2340 self._splitter_main.SetSashPosition(splitter_width / 2, True)
2341
2343 self._LCTRL_problems.set_string_items()
2344 self._TCTRL_soap_problem.SetValue(u'')
2345 self._TCTRL_soap.SetValue(u'')
2346 self._CHBOX_filter_by_problem.SetLabel(_('&Filter by problem'))
2347 self._TCTRL_journal.SetValue(u'')
2348
2350 if not self.__pat.connected:
2351 return None
2352
2353 if self.__problem is None:
2354 return None
2355
2356 saved = self.__pat.emr.add_clin_narrative (
2357 note = self._TCTRL_soap.GetValue().strip(),
2358 soap_cat = u'u',
2359 episode = self.__problem
2360 )
2361
2362 if saved is None:
2363 return False
2364
2365 self._TCTRL_soap.SetValue(u'')
2366 self.__refresh_journal()
2367 return True
2368
2370 if self._TCTRL_soap.GetValue().strip() == u'':
2371 return True
2372 save_it = gmGuiHelpers.gm_show_question (
2373 title = _('Saving SOAP note'),
2374 question = _('Do you want to save the SOAP note ?')
2375 )
2376 if save_it:
2377 return self.__save_soap()
2378 return False
2379
2390
2411
2412
2413
2415 """Configure enabled event signals."""
2416
2417 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
2418 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
2419 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db)
2420 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
2421
2422
2423 self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback)
2424 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
2425
2427 """Another patient is about to be activated.
2428
2429 Patient change will not proceed before this returns True.
2430 """
2431 if not self.__pat.connected:
2432 return True
2433 self.__perhaps_save_soap()
2434 self.__problem = None
2435 return True
2436
2438 """The client is about to be shut down.
2439
2440 Shutdown will not proceed before this returns.
2441 """
2442 if not self.__pat.connected:
2443 return
2444 if not self.__save_soap():
2445 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save SimpleNotes SOAP note.'), beep = True)
2446 return
2447
2449 wx.CallAfter(self.__reset_ui)
2450
2452 wx.CallAfter(self._schedule_data_reget)
2453
2455 wx.CallAfter(self._schedule_data_reget)
2456
2458 self.__perhaps_save_soap()
2459 epi = self._LCTRL_problems.get_selected_item_data(only_one = True)
2460 self._TCTRL_soap_problem.SetValue(_('Progress note: %s%s') % (
2461 epi['description'],
2462 gmTools.coalesce(epi['health_issue'], u'', u' (%s)')
2463 ))
2464 self.__problem = epi
2465 self._TCTRL_soap.SetValue(u'')
2466
2481
2483 event.Skip()
2484 self.__refresh_journal()
2485
2487 event.Skip()
2488 self.__refresh_journal()
2489
2504
2511
2519
2523
2527
2528
2529
2531 self.__refresh_problem_list()
2532 self.__refresh_journal()
2533 self._TCTRL_soap.SetValue(u'')
2534 return True
2535
2536
2537
2538
2539 if __name__ == '__main__':
2540
2541 if len(sys.argv) < 2:
2542 sys.exit()
2543
2544 if sys.argv[1] != 'test':
2545 sys.exit()
2546
2547 gmI18N.activate_locale()
2548 gmI18N.install_domain(domain = 'gnumed')
2549
2550
2559
2566
2579
2580
2581 test_cSoapNoteExpandoEditAreaPnl()
2582
2583
2584
2585