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