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
43 from Gnumed.exporters import gmPatientExporter
44
45
46 _log = logging.getLogger('gm.ui')
47 _log.info(__version__)
48
49
50
52
53
54 if patient is None:
55 patient = gmPerson.gmCurrentPatient()
56
57 if not patient.connected:
58 gmDispatcher.send(signal = 'statustext', msg = _('Cannot move progress notes. No active patient.'))
59 return False
60
61 if parent is None:
62 parent = wx.GetApp().GetTopWindow()
63
64 emr = patient.get_emr()
65
66 if encounters is None:
67 encs = emr.get_encounters(episodes = episodes)
68 encounters = gmEMRStructWidgets.select_encounters (
69 parent = parent,
70 patient = patient,
71 single_selection = False,
72 encounters = encs
73 )
74
75 if encounters is None:
76 return True
77
78 if len(encounters) == 0:
79 return True
80
81 notes = emr.get_clin_narrative (
82 encounters = encounters,
83 episodes = episodes
84 )
85
86
87 if move_all:
88 selected_narr = notes
89 else:
90 selected_narr = gmListWidgets.get_choices_from_list (
91 parent = parent,
92 caption = _('Moving progress notes between encounters ...'),
93 single_selection = False,
94 can_return_empty = True,
95 data = notes,
96 msg = _('\n Select the progress notes to move from the list !\n\n'),
97 columns = [_('when'), _('who'), _('type'), _('entry')],
98 choices = [
99 [ narr['date'].strftime('%x %H:%M'),
100 narr['provider'],
101 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
102 narr['narrative'].replace('\n', '/').replace('\r', '/')
103 ] for narr in notes
104 ]
105 )
106
107 if not selected_narr:
108 return True
109
110
111 enc2move2 = gmEMRStructWidgets.select_encounters (
112 parent = parent,
113 patient = patient,
114 single_selection = True
115 )
116
117 if not enc2move2:
118 return True
119
120 for narr in selected_narr:
121 narr['pk_encounter'] = enc2move2['pk_encounter']
122 narr.save()
123
124 return True
125
127
128
129 if patient is None:
130 patient = gmPerson.gmCurrentPatient()
131
132 if not patient.connected:
133 gmDispatcher.send(signal = 'statustext', msg = _('Cannot edit progress notes. No active patient.'))
134 return False
135
136 if parent is None:
137 parent = wx.GetApp().GetTopWindow()
138
139 emr = patient.get_emr()
140
141 def delete(item):
142 if item is None:
143 return False
144 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
145 parent,
146 -1,
147 caption = _('Deleting progress note'),
148 question = _(
149 'Are you positively sure you want to delete this\n'
150 'progress note from the medical record ?\n'
151 '\n'
152 'Note that even if you chose to delete the entry it will\n'
153 'still be (invisibly) kept in the audit trail to protect\n'
154 'you from litigation because physical deletion is known\n'
155 'to be unlawful in some jurisdictions.\n'
156 ),
157 button_defs = (
158 {'label': _('Delete'), 'tooltip': _('Yes, delete the progress note.'), 'default': False},
159 {'label': _('Cancel'), 'tooltip': _('No, do NOT delete the progress note.'), 'default': True}
160 )
161 )
162 decision = dlg.ShowModal()
163
164 if decision != wx.ID_YES:
165 return False
166
167 gmClinNarrative.delete_clin_narrative(narrative = item['pk_narrative'])
168 return True
169
170 def edit(item):
171 if item is None:
172 return False
173
174 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
175 parent,
176 -1,
177 title = _('Editing progress note'),
178 msg = _('This is the original progress note:'),
179 data = item.format(left_margin = u' ', fancy = True),
180 text = item['narrative']
181 )
182 decision = dlg.ShowModal()
183
184 if decision != wx.ID_SAVE:
185 return False
186
187 val = dlg.value
188 dlg.Destroy()
189 if val.strip() == u'':
190 return False
191
192 item['narrative'] = val
193 item.save_payload()
194
195 return True
196
197 def refresh(lctrl):
198 notes = emr.get_clin_narrative (
199 encounters = encounters,
200 episodes = episodes,
201 providers = [ gmStaff.gmCurrentProvider()['short_alias'] ]
202 )
203 lctrl.set_string_items(items = [
204 [ narr['date'].strftime('%x %H:%M'),
205 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
206 narr['narrative'].replace('\n', '/').replace('\r', '/')
207 ] for narr in notes
208 ])
209 lctrl.set_data(data = notes)
210
211
212 gmListWidgets.get_choices_from_list (
213 parent = parent,
214 caption = _('Managing progress notes'),
215 msg = _(
216 '\n'
217 ' This list shows the progress notes by %s.\n'
218 '\n'
219 ) % gmStaff.gmCurrentProvider()['short_alias'],
220 columns = [_('when'), _('type'), _('entry')],
221 single_selection = True,
222 can_return_empty = False,
223 edit_callback = edit,
224 delete_callback = delete,
225 refresh_callback = refresh,
226 ignore_OK_button = True
227 )
228
230
231 if parent is None:
232 parent = wx.GetApp().GetTopWindow()
233
234 searcher = wx.TextEntryDialog (
235 parent = parent,
236 message = _('Enter (regex) term to search for across all EMRs:'),
237 caption = _('Text search across all EMRs'),
238 style = wx.OK | wx.CANCEL | wx.CENTRE
239 )
240 result = searcher.ShowModal()
241
242 if result != wx.ID_OK:
243 return
244
245 wx.BeginBusyCursor()
246 term = searcher.GetValue()
247 searcher.Destroy()
248 results = gmClinNarrative.search_text_across_emrs(search_term = term)
249 wx.EndBusyCursor()
250
251 if len(results) == 0:
252 gmGuiHelpers.gm_show_info (
253 _(
254 'Nothing found for search term:\n'
255 ' "%s"'
256 ) % term,
257 _('Search results')
258 )
259 return
260
261 items = [ [gmPerson.cIdentity(aPK_obj =
262 r['pk_patient'])['description_gender'], r['narrative'],
263 r['src_table']] for r in results ]
264
265 selected_patient = gmListWidgets.get_choices_from_list (
266 parent = parent,
267 caption = _('Search results for %s') % term,
268 choices = items,
269 columns = [_('Patient'), _('Match'), _('Match location')],
270 data = [ r['pk_patient'] for r in results ],
271 single_selection = True,
272 can_return_empty = False
273 )
274
275 if selected_patient is None:
276 return
277
278 wx.CallAfter(gmPatSearchWidgets.set_active_patient, patient = gmPerson.cIdentity(aPK_obj = selected_patient))
279
281
282
283 if patient is None:
284 patient = gmPerson.gmCurrentPatient()
285
286 if not patient.connected:
287 gmDispatcher.send(signal = 'statustext', msg = _('Cannot search EMR. No active patient.'))
288 return False
289
290 if parent is None:
291 parent = wx.GetApp().GetTopWindow()
292
293 searcher = wx.TextEntryDialog (
294 parent = parent,
295 message = _('Enter search term:'),
296 caption = _('Text search of entire EMR of active patient'),
297 style = wx.OK | wx.CANCEL | wx.CENTRE
298 )
299 result = searcher.ShowModal()
300
301 if result != wx.ID_OK:
302 searcher.Destroy()
303 return False
304
305 wx.BeginBusyCursor()
306 val = searcher.GetValue()
307 searcher.Destroy()
308 emr = patient.get_emr()
309 rows = emr.search_narrative_simple(val)
310 wx.EndBusyCursor()
311
312 if len(rows) == 0:
313 gmGuiHelpers.gm_show_info (
314 _(
315 'Nothing found for search term:\n'
316 ' "%s"'
317 ) % val,
318 _('Search results')
319 )
320 return True
321
322 txt = u''
323 for row in rows:
324 txt += u'%s: %s\n' % (
325 row['soap_cat'],
326 row['narrative']
327 )
328
329 txt += u' %s: %s - %s %s\n' % (
330 _('Encounter'),
331 row['encounter_started'].strftime('%x %H:%M'),
332 row['encounter_ended'].strftime('%H:%M'),
333 row['encounter_type']
334 )
335 txt += u' %s: %s\n' % (
336 _('Episode'),
337 row['episode']
338 )
339 txt += u' %s: %s\n\n' % (
340 _('Health issue'),
341 row['health_issue']
342 )
343
344 msg = _(
345 'Search term was: "%s"\n'
346 '\n'
347 'Search results:\n\n'
348 '%s\n'
349 ) % (val, txt)
350
351 dlg = wx.MessageDialog (
352 parent = parent,
353 message = msg,
354 caption = _('Search results for %s') % val,
355 style = wx.OK | wx.STAY_ON_TOP
356 )
357 dlg.ShowModal()
358 dlg.Destroy()
359
360 return True
361
363
364
365 pat = gmPerson.gmCurrentPatient()
366 if not pat.connected:
367 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR for Medistar. No active patient.'))
368 return False
369
370 if encounter is None:
371 encounter = pat.get_emr().active_encounter
372
373 if parent is None:
374 parent = wx.GetApp().GetTopWindow()
375
376
377 aWildcard = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files"))
378
379 aDefDir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed','export')))
380
381 fname = '%s-%s-%s-%s-%s.txt' % (
382 'Medistar-MD',
383 time.strftime('%Y-%m-%d',time.localtime()),
384 pat['lastnames'].replace(' ', '-'),
385 pat['firstnames'].replace(' ', '_'),
386 pat.get_formatted_dob(format = '%Y-%m-%d')
387 )
388 dlg = wx.FileDialog (
389 parent = parent,
390 message = _("Save EMR extract for MEDISTAR import as..."),
391 defaultDir = aDefDir,
392 defaultFile = fname,
393 wildcard = aWildcard,
394 style = wx.SAVE
395 )
396 choice = dlg.ShowModal()
397 fname = dlg.GetPath()
398 dlg.Destroy()
399 if choice != wx.ID_OK:
400 return False
401
402 wx.BeginBusyCursor()
403 _log.debug('exporting encounter for medistar import to [%s]', fname)
404 exporter = gmPatientExporter.cMedistarSOAPExporter()
405 successful, fname = exporter.export_to_file (
406 filename = fname,
407 encounter = encounter,
408 soap_cats = u'soap',
409 export_to_import_file = True
410 )
411 if not successful:
412 gmGuiHelpers.gm_show_error (
413 _('Error exporting progress notes for MEDISTAR import.'),
414 _('MEDISTAR progress notes export')
415 )
416 wx.EndBusyCursor()
417 return False
418
419 gmDispatcher.send(signal = 'statustext', msg = _('Successfully exported progress notes into file [%s] for Medistar import.') % fname, beep=False)
420
421 wx.EndBusyCursor()
422 return True
423
425 """soap_cats needs to be a list"""
426
427 if parent is None:
428 parent = wx.GetApp().GetTopWindow()
429
430 pat = gmPerson.gmCurrentPatient()
431 emr = pat.get_emr()
432
433 selected_soap = {}
434 selected_narrative_pks = []
435
436
437 def pick_soap_from_episode(episode):
438
439 narr_for_epi = emr.get_clin_narrative(episodes = [episode['pk_episode']], soap_cats = soap_cats)
440
441 if len(narr_for_epi) == 0:
442 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episode.'))
443 return True
444
445 dlg = cNarrativeListSelectorDlg (
446 parent = parent,
447 id = -1,
448 narrative = narr_for_epi,
449 msg = _(
450 '\n This is the narrative (type %s) for the chosen episodes.\n'
451 '\n'
452 ' Now, mark the entries you want to include in your report.\n'
453 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soap')) ])
454 )
455
456
457
458
459
460
461 btn_pressed = dlg.ShowModal()
462 selected_narr = dlg.get_selected_item_data()
463 dlg.Destroy()
464
465 if btn_pressed == wx.ID_CANCEL:
466 return True
467
468 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ]
469 for narr in selected_narr:
470 selected_soap[narr['pk_narrative']] = narr
471
472 print "before returning from picking soap"
473
474 return True
475
476 selected_episode_pks = []
477
478 all_epis = [ epi for epi in emr.get_episodes() if epi.has_narrative ]
479
480 if len(all_epis) == 0:
481 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.'))
482 return []
483
484 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg (
485 parent = parent,
486 id = -1,
487 episodes = all_epis,
488 msg = _('\n Select the the episode you want to report on.\n')
489 )
490
491
492
493
494
495
496 dlg.left_extra_button = (
497 _('Pick SOAP'),
498 _('Pick SOAP entries from topmost selected episode'),
499 pick_soap_from_episode
500 )
501 btn_pressed = dlg.ShowModal()
502 dlg.Destroy()
503
504 if btn_pressed == wx.ID_CANCEL:
505 return None
506
507 return selected_soap.values()
508
510 """soap_cats needs to be a list"""
511
512 pat = gmPerson.gmCurrentPatient()
513 emr = pat.get_emr()
514
515 if parent is None:
516 parent = wx.GetApp().GetTopWindow()
517
518 selected_soap = {}
519 selected_issue_pks = []
520 selected_episode_pks = []
521 selected_narrative_pks = []
522
523 while 1:
524
525 all_issues = emr.get_health_issues()
526 all_issues.insert(0, gmEMRStructItems.get_dummy_health_issue())
527 dlg = gmEMRStructWidgets.cIssueListSelectorDlg (
528 parent = parent,
529 id = -1,
530 issues = all_issues,
531 msg = _('\n In the list below mark the health issues you want to report on.\n')
532 )
533 selection_idxs = []
534 for idx in range(len(all_issues)):
535 if all_issues[idx]['pk_health_issue'] in selected_issue_pks:
536 selection_idxs.append(idx)
537 if len(selection_idxs) != 0:
538 dlg.set_selections(selections = selection_idxs)
539 btn_pressed = dlg.ShowModal()
540 selected_issues = dlg.get_selected_item_data()
541 dlg.Destroy()
542
543 if btn_pressed == wx.ID_CANCEL:
544 return selected_soap.values()
545
546 selected_issue_pks = [ i['pk_health_issue'] for i in selected_issues ]
547
548 while 1:
549
550 all_epis = emr.get_episodes(issues = selected_issue_pks)
551
552 if len(all_epis) == 0:
553 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.'))
554 break
555
556 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg (
557 parent = parent,
558 id = -1,
559 episodes = all_epis,
560 msg = _(
561 '\n These are the episodes known for the health issues just selected.\n\n'
562 ' Now, mark the the episodes you want to report on.\n'
563 )
564 )
565 selection_idxs = []
566 for idx in range(len(all_epis)):
567 if all_epis[idx]['pk_episode'] in selected_episode_pks:
568 selection_idxs.append(idx)
569 if len(selection_idxs) != 0:
570 dlg.set_selections(selections = selection_idxs)
571 btn_pressed = dlg.ShowModal()
572 selected_epis = dlg.get_selected_item_data()
573 dlg.Destroy()
574
575 if btn_pressed == wx.ID_CANCEL:
576 break
577
578 selected_episode_pks = [ i['pk_episode'] for i in selected_epis ]
579
580
581 all_narr = emr.get_clin_narrative(episodes = selected_episode_pks, soap_cats = soap_cats)
582
583 if len(all_narr) == 0:
584 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episodes.'))
585 continue
586
587 dlg = cNarrativeListSelectorDlg (
588 parent = parent,
589 id = -1,
590 narrative = all_narr,
591 msg = _(
592 '\n This is the narrative (type %s) for the chosen episodes.\n\n'
593 ' Now, mark the entries you want to include in your report.\n'
594 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soap')) ])
595 )
596 selection_idxs = []
597 for idx in range(len(all_narr)):
598 if all_narr[idx]['pk_narrative'] in selected_narrative_pks:
599 selection_idxs.append(idx)
600 if len(selection_idxs) != 0:
601 dlg.set_selections(selections = selection_idxs)
602 btn_pressed = dlg.ShowModal()
603 selected_narr = dlg.get_selected_item_data()
604 dlg.Destroy()
605
606 if btn_pressed == wx.ID_CANCEL:
607 continue
608
609 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ]
610 for narr in selected_narr:
611 selected_soap[narr['pk_narrative']] = narr
612
614
616
617 narrative = kwargs['narrative']
618 del kwargs['narrative']
619
620 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs)
621
622 self.SetTitle(_('Select the narrative you are interested in ...'))
623
624 self._LCTRL_items.set_columns([_('when'), _('who'), _('type'), _('entry')])
625
626 self._LCTRL_items.set_string_items (
627 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 ]
628 )
629 self._LCTRL_items.set_column_widths()
630 self._LCTRL_items.set_data(data = narrative)
631
632 from Gnumed.wxGladeWidgets import wxgMoveNarrativeDlg
633
635
637
638 self.encounter = kwargs['encounter']
639 self.source_episode = kwargs['episode']
640 del kwargs['encounter']
641 del kwargs['episode']
642
643 wxgMoveNarrativeDlg.wxgMoveNarrativeDlg.__init__(self, *args, **kwargs)
644
645 self.LBL_source_episode.SetLabel(u'%s%s' % (self.source_episode['description'], gmTools.coalesce(self.source_episode['health_issue'], u'', u' (%s)')))
646 self.LBL_encounter.SetLabel('%s: %s %s - %s' % (
647 self.encounter['started'].strftime('%x').decode(gmI18N.get_encoding()),
648 self.encounter['l10n_type'],
649 self.encounter['started'].strftime('%H:%M'),
650 self.encounter['last_affirmed'].strftime('%H:%M')
651 ))
652 pat = gmPerson.gmCurrentPatient()
653 emr = pat.get_emr()
654 narr = emr.get_clin_narrative(episodes=[self.source_episode['pk_episode']], encounters=[self.encounter['pk_encounter']])
655 if len(narr) == 0:
656 narr = [{'narrative': _('There is no narrative for this episode in this encounter.')}]
657 self.LBL_narrative.SetLabel(u'\n'.join([n['narrative'] for n in narr]))
658
659
681
682 from Gnumed.wxGladeWidgets import wxgSoapPluginPnl
683
684 -class cSoapPluginPnl(wxgSoapPluginPnl.wxgSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
685 """A panel for in-context editing of progress notes.
686
687 Expects to be used as a notebook page.
688
689 Left hand side:
690 - problem list (health issues and active episodes)
691 - previous notes
692
693 Right hand side:
694 - encounter details fields
695 - notebook with progress note editors
696 - visual progress notes
697 - hints
698
699 Listens to patient change signals, thus acts on the current patient.
700 """
712
713
714
716
717 if not self.__encounter_valid_for_save():
718 return False
719
720 emr = self.__pat.get_emr()
721 enc = emr.active_encounter
722
723 rfe = self._TCTRL_rfe.GetValue().strip()
724 if len(rfe) == 0:
725 enc['reason_for_encounter'] = None
726 else:
727 enc['reason_for_encounter'] = rfe
728 aoe = self._TCTRL_aoe.GetValue().strip()
729 if len(aoe) == 0:
730 enc['assessment_of_encounter'] = None
731 else:
732 enc['assessment_of_encounter'] = aoe
733
734 enc.save_payload()
735
736 enc.generic_codes_rfe = [ c['data'] for c in self._PRW_rfe_codes.GetData() ]
737 enc.generic_codes_aoe = [ c['data'] for c in self._PRW_aoe_codes.GetData() ]
738
739 return True
740
741
742
744 self._LCTRL_active_problems.set_columns([_('Last'), _('Problem'), _('In health issue')])
745 self._LCTRL_active_problems.set_string_items()
746
747 self._splitter_main.SetSashGravity(0.5)
748 self._splitter_left.SetSashGravity(0.5)
749
750 splitter_size = self._splitter_main.GetSizeTuple()[0]
751 self._splitter_main.SetSashPosition(splitter_size * 3 / 10, True)
752
753 splitter_size = self._splitter_left.GetSizeTuple()[1]
754 self._splitter_left.SetSashPosition(splitter_size * 6 / 20, True)
755
756 self._NB_soap_editors.DeleteAllPages()
757 self._NB_soap_editors.MoveAfterInTabOrder(self._PRW_aoe_codes)
758
760 start = self._PRW_encounter_start.GetData()
761 if start is None:
762 return
763 start = start.get_pydt()
764
765 end = self._PRW_encounter_end.GetData()
766 if end is None:
767 fts = gmDateTime.cFuzzyTimestamp (
768 timestamp = start,
769 accuracy = gmDateTime.acc_minutes
770 )
771 self._PRW_encounter_end.SetText(fts.format_accurately(), data = fts)
772 return
773 end = end.get_pydt()
774
775 if start > end:
776 end = end.replace (
777 year = start.year,
778 month = start.month,
779 day = start.day
780 )
781 fts = gmDateTime.cFuzzyTimestamp (
782 timestamp = end,
783 accuracy = gmDateTime.acc_minutes
784 )
785 self._PRW_encounter_end.SetText(fts.format_accurately(), data = fts)
786 return
787
788 emr = self.__pat.get_emr()
789 if start != emr.active_encounter['started']:
790 end = end.replace (
791 year = start.year,
792 month = start.month,
793 day = start.day
794 )
795 fts = gmDateTime.cFuzzyTimestamp (
796 timestamp = end,
797 accuracy = gmDateTime.acc_minutes
798 )
799 self._PRW_encounter_end.SetText(fts.format_accurately(), data = fts)
800 return
801
802 return
803
805 """Clear all information from input panel."""
806
807 self._LCTRL_active_problems.set_string_items()
808
809 self._TCTRL_recent_notes.SetValue(u'')
810 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on selected problem'))
811
812 self._TCTRL_rfe.SetValue(u'')
813 self._PRW_rfe_codes.SetText(suppress_smarts = True)
814 self._TCTRL_aoe.SetValue(u'')
815 self._PRW_aoe_codes.SetText(suppress_smarts = True)
816
817 self._NB_soap_editors.DeleteAllPages()
818 self._NB_soap_editors.add_editor()
819
821 """Update health problems list."""
822
823 self._LCTRL_active_problems.set_string_items()
824
825 emr = self.__pat.get_emr()
826 problems = emr.get_problems (
827 include_closed_episodes = self._CHBOX_show_closed_episodes.IsChecked(),
828 include_irrelevant_issues = self._CHBOX_irrelevant_issues.IsChecked()
829 )
830
831 list_items = []
832 active_problems = []
833 for problem in problems:
834 if not problem['problem_active']:
835 if not problem['is_potential_problem']:
836 continue
837
838 active_problems.append(problem)
839
840 if problem['type'] == 'issue':
841 issue = emr.problem2issue(problem)
842 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue'])
843 if last_encounter is None:
844 last = issue['modified_when'].strftime('%m/%Y')
845 else:
846 last = last_encounter['last_affirmed'].strftime('%m/%Y')
847
848 list_items.append([last, problem['problem'], gmTools.u_down_left_arrow])
849
850 elif problem['type'] == 'episode':
851 epi = emr.problem2episode(problem)
852 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode'])
853 if last_encounter is None:
854 last = epi['episode_modified_when'].strftime('%m/%Y')
855 else:
856 last = last_encounter['last_affirmed'].strftime('%m/%Y')
857
858 list_items.append ([
859 last,
860 problem['problem'],
861 gmTools.coalesce(initial = epi['health_issue'], instead = u'?')
862 ])
863
864 self._LCTRL_active_problems.set_string_items(items = list_items)
865 self._LCTRL_active_problems.set_column_widths()
866 self._LCTRL_active_problems.set_data(data = active_problems)
867
868 showing_potential_problems = (
869 self._CHBOX_show_closed_episodes.IsChecked()
870 or
871 self._CHBOX_irrelevant_issues.IsChecked()
872 )
873 if showing_potential_problems:
874 self._SZR_problem_list_staticbox.SetLabel(_('%s (active+potential) problems') % len(list_items))
875 else:
876 self._SZR_problem_list_staticbox.SetLabel(_('%s active problems') % len(list_items))
877
878 return True
879
881 soap = u''
882 emr = self.__pat.get_emr()
883 prev_enc = emr.get_last_but_one_encounter(issue_id = problem['pk_health_issue'])
884 if prev_enc is not None:
885 soap += prev_enc.format (
886 issues = [ problem['pk_health_issue'] ],
887 with_soap = True,
888 with_docs = False,
889 with_tests = False,
890 patient = self.__pat,
891 fancy_header = False,
892 with_rfe_aoe = True
893 )
894
895 tmp = emr.active_encounter.format_soap (
896 soap_cats = 'soap',
897 emr = emr,
898 issues = [ problem['pk_health_issue'] ],
899 )
900 if len(tmp) > 0:
901 soap += _('Current encounter:') + u'\n'
902 soap += u'\n'.join(tmp) + u'\n'
903
904 if problem['summary'] is not None:
905 soap += u'\n-- %s ----------\n%s' % (
906 _('Cumulative summary'),
907 gmTools.wrap (
908 text = problem['summary'],
909 width = 45,
910 initial_indent = u' ',
911 subsequent_indent = u' '
912 ).strip('\n')
913 )
914
915 return soap
916
918 soap = u''
919 emr = self.__pat.get_emr()
920 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_episode'])
921 if prev_enc is not None:
922 soap += prev_enc.format (
923 episodes = [ problem['pk_episode'] ],
924 with_soap = True,
925 with_docs = False,
926 with_tests = False,
927 patient = self.__pat,
928 fancy_header = False,
929 with_rfe_aoe = True
930 )
931 else:
932 if problem['pk_health_issue'] is not None:
933 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_health_issue'])
934 if prev_enc is not None:
935 soap += prev_enc.format (
936 with_soap = True,
937 with_docs = False,
938 with_tests = False,
939 patient = self.__pat,
940 issues = [ problem['pk_health_issue'] ],
941 fancy_header = False,
942 with_rfe_aoe = True
943 )
944
945 tmp = emr.active_encounter.format_soap (
946 soap_cats = 'soap',
947 emr = emr,
948 issues = [ problem['pk_health_issue'] ],
949 )
950 if len(tmp) > 0:
951 soap += _('Current encounter:') + u'\n'
952 soap += u'\n'.join(tmp) + u'\n'
953
954 if problem['summary'] is not None:
955 soap += u'\n-- %s ----------\n%s' % (
956 _('Cumulative summary'),
957 gmTools.wrap (
958 text = problem['summary'],
959 width = 45,
960 initial_indent = u' ',
961 subsequent_indent = u' '
962 ).strip('\n')
963 )
964
965 return soap
966
969
993
995 """This refreshes the recent-notes part."""
996
997 soap = u''
998 caption = u'<?>'
999
1000 if problem['type'] == u'issue':
1001 caption = problem['problem'][:35]
1002 soap = self.__get_soap_for_issue_problem(problem = problem)
1003
1004 elif problem['type'] == u'episode':
1005 caption = problem['problem'][:35]
1006 soap = self.__get_soap_for_episode_problem(problem = problem)
1007
1008 self._TCTRL_recent_notes.SetValue(soap)
1009 self._TCTRL_recent_notes.ShowPosition(self._TCTRL_recent_notes.GetLastPosition())
1010 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on %s%s%s') % (
1011 gmTools.u_left_double_angle_quote,
1012 caption,
1013 gmTools.u_right_double_angle_quote
1014 ))
1015
1016 self._TCTRL_recent_notes.Refresh()
1017
1018 return True
1019
1038
1040 """Assumes that the field data is valid."""
1041
1042 emr = self.__pat.get_emr()
1043 enc = emr.active_encounter
1044
1045 data = {
1046 'pk_type': enc['pk_type'],
1047 'reason_for_encounter': gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u''),
1048 'assessment_of_encounter': gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
1049 'pk_location': enc['pk_location'],
1050 'pk_patient': enc['pk_patient'],
1051 'pk_generic_codes_rfe': self._PRW_rfe_codes.GetData(),
1052 'pk_generic_codes_aoe': self._PRW_aoe_codes.GetData(),
1053 'started': enc['started'],
1054 'last_affirmed': enc['last_affirmed']
1055 }
1056
1057 return not enc.same_payload(another_object = data)
1058
1061
1062
1063
1065 """Configure enabled event signals."""
1066
1067 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1068 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1069 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db)
1070 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
1071 gmDispatcher.connect(signal = u'episode_code_mod_db', receiver = self._on_episode_issue_mod_db)
1072 gmDispatcher.connect(signal = u'doc_mod_db', receiver = self._on_doc_mod_db)
1073 gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified)
1074 gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_switched)
1075 gmDispatcher.connect(signal = u'rfe_code_mod_db', receiver = self._on_encounter_code_modified)
1076 gmDispatcher.connect(signal = u'aoe_code_mod_db', receiver = self._on_encounter_code_modified)
1077
1078
1079 self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback)
1080 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
1081
1083 """Another patient is about to be activated.
1084
1085 Patient change will not proceed before this returns True.
1086 """
1087
1088
1089 if not self.__pat.connected:
1090 return True
1091 return self._NB_soap_editors.warn_on_unsaved_soap()
1092
1094 """The client is about to be shut down.
1095
1096 Shutdown will not proceed before this returns.
1097 """
1098 if not self.__pat.connected:
1099 return True
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115 emr = self.__pat.get_emr()
1116 saved = self._NB_soap_editors.save_all_editors (
1117 emr = emr,
1118 episode_name_candidates = [
1119 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
1120 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'')
1121 ]
1122 )
1123 if not saved:
1124 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
1125 return True
1126
1128 wx.CallAfter(self.__on_pre_patient_selection)
1129
1131 self.__reset_ui_content()
1132
1134 wx.CallAfter(self._schedule_data_reget)
1135 self.__patient_just_changed = True
1136
1138 wx.CallAfter(self.__refresh_current_editor)
1139
1141 wx.CallAfter(self._schedule_data_reget)
1142
1147
1149 wx.CallAfter(self.__refresh_encounter)
1150
1152 wx.CallAfter(self.__on_current_encounter_switched)
1153
1155 self.__refresh_encounter()
1156
1157
1158
1160 """Show related note at the bottom."""
1161 pass
1162
1174
1176 """Show related note at the bottom."""
1177 emr = self.__pat.get_emr()
1178 self.__refresh_recent_notes (
1179 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1180 )
1181
1183 """Open progress note editor for this problem.
1184 """
1185 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1186 if problem is None:
1187 return True
1188
1189 dbcfg = gmCfg.cCfgSQL()
1190 allow_duplicate_editors = bool(dbcfg.get2 (
1191 option = u'horstspace.soap_editor.allow_same_episode_multiple_times',
1192 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1193 bias = u'user',
1194 default = False
1195 ))
1196 if self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors):
1197 return True
1198
1199 gmGuiHelpers.gm_show_error (
1200 aMessage = _(
1201 'Cannot open progress note editor for\n\n'
1202 '[%s].\n\n'
1203 ) % problem['problem'],
1204 aTitle = _('opening progress note editor')
1205 )
1206 event.Skip()
1207 return False
1208
1210 self.__refresh_problem_list()
1211
1213 self.__refresh_problem_list()
1214
1215
1216
1220
1224
1228
1239
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
1572 if self.problem is None:
1573 self._PNL_visual_soap.refresh(document_folder = None)
1574 return
1575
1576 if self.problem['type'] == u'issue':
1577 self._PNL_visual_soap.refresh(document_folder = None)
1578 return
1579
1580 if self.problem['type'] == u'episode':
1581 pat = gmPerson.gmCurrentPatient()
1582 doc_folder = pat.get_document_folder()
1583 emr = pat.get_emr()
1584 self._PNL_visual_soap.refresh (
1585 document_folder = doc_folder,
1586 episodes = [self.problem['pk_episode']],
1587 encounter = emr.active_encounter['pk_encounter']
1588 )
1589 return
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
1734
1735
1736
1737
1738 self.FitInside()
1739
1740 if self.HasScrollbar(wx.VERTICAL):
1741
1742 expando = self.FindWindowById(evt.GetId())
1743 y_expando = expando.GetPositionTuple()[1]
1744 h_expando = expando.GetSizeTuple()[1]
1745 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1
1746 y_cursor = int(round((float(line_cursor) / expando.NumberOfLines) * h_expando))
1747 y_desired_visible = y_expando + y_cursor
1748
1749 y_view = self.ViewStart[1]
1750 h_view = self.GetClientSizeTuple()[1]
1751
1752
1753
1754
1755
1756
1757
1758
1759 if y_desired_visible < y_view:
1760
1761 self.Scroll(0, y_desired_visible)
1762
1763 if y_desired_visible > h_view:
1764
1765 self.Scroll(0, y_desired_visible)
1766
1767
1768
1770 soap_notes = []
1771
1772 tmp = self._TCTRL_Soap.GetValue().strip()
1773 if tmp != u'':
1774 soap_notes.append(['s', tmp])
1775
1776 tmp = self._TCTRL_sOap.GetValue().strip()
1777 if tmp != u'':
1778 soap_notes.append(['o', tmp])
1779
1780 tmp = self._TCTRL_soAp.GetValue().strip()
1781 if tmp != u'':
1782 soap_notes.append(['a', tmp])
1783
1784 tmp = self._TCTRL_soaP.GetValue().strip()
1785 if tmp != u'':
1786 soap_notes.append(['p', tmp])
1787
1788 return soap_notes
1789
1790 soap = property(_get_soap, lambda x:x)
1791
1793
1794
1795 for field in self.soap_fields:
1796 if field.GetValue().strip() != u'':
1797 return False
1798
1799
1800 summary = self._TCTRL_episode_summary.GetValue().strip()
1801 if self.problem is None:
1802 if summary != u'':
1803 return False
1804 elif self.problem['type'] == u'issue':
1805 if summary != u'':
1806 return False
1807 else:
1808 if self.problem['summary'] is None:
1809 if summary != u'':
1810 return False
1811 else:
1812 if summary != self.problem['summary'].strip():
1813 return False
1814
1815
1816 new_codes = self._PRW_episode_codes.GetData()
1817 if self.problem is None:
1818 if len(new_codes) > 0:
1819 return False
1820 elif self.problem['type'] == u'issue':
1821 if len(new_codes) > 0:
1822 return False
1823 else:
1824 old_code_pks = self.problem.generic_codes
1825 if len(old_code_pks) != len(new_codes):
1826 return False
1827 for code in new_codes:
1828 if code['data'] not in old_code_pks:
1829 return False
1830
1831 return True
1832
1833 empty = property(_get_empty, lambda x:x)
1834
1835 -class cSoapLineTextCtrl(wx_expando.ExpandoTextCtrl):
1836
1837 - def __init__(self, *args, **kwargs):
1838
1839 wx_expando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
1840
1841 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+")
1842
1843 self.__register_interests()
1844
1845
1846
1847 - def _wrapLine(self, line, dc, width):
1848
1849 if (wx.MAJOR_VERSION >= 2) and (wx.MINOR_VERSION > 8):
1850 return super(cSoapLineTextCtrl, self)._wrapLine(line, dc, width)
1851
1852
1853
1854
1855 pte = dc.GetPartialTextExtents(line)
1856 width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X)
1857 idx = 0
1858 start = 0
1859 count = 0
1860 spc = -1
1861 while idx < len(pte):
1862 if line[idx] == ' ':
1863 spc = idx
1864 if pte[idx] - start > width:
1865
1866 count += 1
1867
1868 if spc != -1:
1869 idx = spc + 1
1870 spc = -1
1871 if idx < len(pte):
1872 start = pte[idx]
1873 else:
1874 idx += 1
1875 return count
1876
1877
1878
1880
1881
1882 wx.EVT_CHAR(self, self.__on_char)
1883 wx.EVT_SET_FOCUS(self, self.__on_focus)
1884
1885 - def __on_focus(self, evt):
1886 evt.Skip()
1887 wx.CallAfter(self._after_on_focus)
1888
1889 - def _after_on_focus(self):
1890
1891 evt = wx.PyCommandEvent(wx_expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
1892 evt.SetEventObject(self)
1893
1894
1895
1896
1897 self.GetEventHandler().ProcessEvent(evt)
1898
1899 - def __on_char(self, evt):
1900 char = unichr(evt.GetUnicodeKey())
1901
1902 if self.LastPosition == 1:
1903 evt.Skip()
1904 return
1905
1906 explicit_expansion = False
1907 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT):
1908 if evt.GetKeyCode() != 13:
1909 evt.Skip()
1910 return
1911 explicit_expansion = True
1912
1913 if not explicit_expansion:
1914 if self.__keyword_separators.match(char) is None:
1915 evt.Skip()
1916 return
1917
1918 caret_pos, line_no = self.PositionToXY(self.InsertionPoint)
1919 line = self.GetLineText(line_no)
1920 word = self.__keyword_separators.split(line[:caret_pos])[-1]
1921
1922 if (
1923 (not explicit_expansion)
1924 and
1925 (word != u'$$steffi')
1926 and
1927 (word not in [ r[0] for r in gmPG2.get_text_expansion_keywords() ])
1928 ):
1929 evt.Skip()
1930 return
1931
1932 start = self.InsertionPoint - len(word)
1933 wx.CallAfter(self.replace_keyword_with_expansion, word, start, explicit_expansion)
1934
1935 evt.Skip()
1936 return
1937
1938 - def replace_keyword_with_expansion(self, keyword=None, position=None, show_list=False):
1939
1940 if show_list:
1941 candidates = gmPG2.get_keyword_expansion_candidates(keyword = keyword)
1942 if len(candidates) == 0:
1943 return
1944 if len(candidates) == 1:
1945 keyword = candidates[0]
1946 else:
1947 keyword = gmListWidgets.get_choices_from_list (
1948 parent = self,
1949 msg = _(
1950 'Several macros match the keyword [%s].\n'
1951 '\n'
1952 'Please select the expansion you want to happen.'
1953 ) % keyword,
1954 caption = _('Selecting text macro'),
1955 choices = candidates,
1956 columns = [_('Keyword')],
1957 single_selection = True,
1958 can_return_empty = False
1959 )
1960 if keyword is None:
1961 return
1962
1963 expansion = gmPG2.expand_keyword(keyword = keyword)
1964
1965 if expansion is None:
1966 return
1967
1968 if expansion == u'':
1969 return
1970
1971 self.Replace (
1972 position,
1973 position + len(keyword),
1974 expansion
1975 )
1976
1977 self.SetInsertionPoint(position + len(expansion) + 1)
1978 self.ShowPosition(position + len(expansion) + 1)
1979
1980 return
1981
1982
1983
2014
2015 cmd = gmCfgWidgets.configure_string_option (
2016 message = _(
2017 'Enter the shell command with which to start\n'
2018 'the image editor for visual progress notes.\n'
2019 '\n'
2020 'Any "%(img)s" included with the arguments\n'
2021 'will be replaced by the file name of the\n'
2022 'note template.'
2023 ),
2024 option = u'external.tools.visual_soap_editor_cmd',
2025 bias = 'user',
2026 default_value = None,
2027 validator = is_valid
2028 )
2029
2030 return cmd
2031
2033 if parent is None:
2034 parent = wx.GetApp().GetTopWindow()
2035
2036 dlg = wx.FileDialog (
2037 parent = parent,
2038 message = _('Choose file to use as template for new visual progress note'),
2039 defaultDir = os.path.expanduser('~'),
2040 defaultFile = '',
2041
2042 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST
2043 )
2044 result = dlg.ShowModal()
2045
2046 if result == wx.ID_CANCEL:
2047 dlg.Destroy()
2048 return None
2049
2050 full_filename = dlg.GetPath()
2051 dlg.Hide()
2052 dlg.Destroy()
2053 return full_filename
2054
2056
2057 if parent is None:
2058 parent = wx.GetApp().GetTopWindow()
2059
2060 dlg = gmGuiHelpers.c3ButtonQuestionDlg (
2061 parent,
2062 -1,
2063 caption = _('Visual progress note source'),
2064 question = _('From which source do you want to pick the image template ?'),
2065 button_defs = [
2066 {'label': _('Database'), 'tooltip': _('List of templates in the database.'), 'default': True},
2067 {'label': _('File'), 'tooltip': _('Files in the filesystem.'), 'default': False},
2068 {'label': _('Device'), 'tooltip': _('Image capture devices (scanners, cameras, etc)'), 'default': False}
2069 ]
2070 )
2071 result = dlg.ShowModal()
2072 dlg.Destroy()
2073
2074
2075 if result == wx.ID_YES:
2076 _log.debug('visual progress note template from: database template')
2077 from Gnumed.wxpython import gmFormWidgets
2078 template = gmFormWidgets.manage_form_templates (
2079 parent = parent,
2080 template_types = [gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE],
2081 active_only = True
2082 )
2083 if template is None:
2084 return (None, None)
2085 filename = template.export_to_file()
2086 if filename is None:
2087 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note template for [%s].') % template['name_long'])
2088 return (None, None)
2089 return (filename, True)
2090
2091
2092 if result == wx.ID_NO:
2093 _log.debug('visual progress note template from: disk file')
2094 fname = select_file_as_visual_progress_note_template(parent = parent)
2095 if fname is None:
2096 return (None, None)
2097
2098 ext = os.path.splitext(fname)[1]
2099 tmp_name = gmTools.get_unique_filename(suffix = ext)
2100 _log.debug('visual progress note from file: [%s] -> [%s]', fname, tmp_name)
2101 shutil.copy2(fname, tmp_name)
2102 return (tmp_name, False)
2103
2104
2105 if result == wx.ID_CANCEL:
2106 _log.debug('visual progress note template from: image capture device')
2107 fnames = gmDocumentWidgets.acquire_images_from_capture_device(device = None, calling_window = parent)
2108 if fnames is None:
2109 return (None, None)
2110 if len(fnames) == 0:
2111 return (None, None)
2112 return (fnames[0], False)
2113
2114 _log.debug('no visual progress note template source selected')
2115 return (None, None)
2116
2118 """This assumes <filename> contains an image which can be handled by the configured image editor."""
2119
2120 if doc_part is not None:
2121 filename = doc_part.export_to_file()
2122 if filename is None:
2123 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.'))
2124 return None
2125
2126 dbcfg = gmCfg.cCfgSQL()
2127 cmd = dbcfg.get2 (
2128 option = u'external.tools.visual_soap_editor_cmd',
2129 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2130 bias = 'user'
2131 )
2132
2133 if cmd is None:
2134 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = False)
2135 cmd = configure_visual_progress_note_editor()
2136 if cmd is None:
2137 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = True)
2138 return None
2139
2140 if u'%(img)s' in cmd:
2141 cmd % {u'img': filename}
2142 else:
2143 cmd = u'%s %s' % (cmd, filename)
2144
2145 if discard_unmodified:
2146 original_stat = os.stat(filename)
2147 original_md5 = gmTools.file2md5(filename)
2148
2149 success = gmShellAPI.run_command_in_shell(cmd, blocking = True)
2150 if not success:
2151 gmGuiHelpers.gm_show_error (
2152 _(
2153 'There was a problem with running the editor\n'
2154 'for visual progress notes.\n'
2155 '\n'
2156 ' [%s]\n'
2157 '\n'
2158 ) % cmd,
2159 _('Editing visual progress note')
2160 )
2161 return None
2162
2163 try:
2164 open(filename, 'r').close()
2165 except StandardError:
2166 _log.exception('problem accessing visual progress note file [%s]', filename)
2167 gmGuiHelpers.gm_show_error (
2168 _(
2169 'There was a problem reading the visual\n'
2170 'progress note from the file:\n'
2171 '\n'
2172 ' [%s]\n'
2173 '\n'
2174 ) % filename,
2175 _('Saving visual progress note')
2176 )
2177 return None
2178
2179 if discard_unmodified:
2180 modified_stat = os.stat(filename)
2181
2182 if original_stat.st_size == modified_stat.st_size:
2183 modified_md5 = gmTools.file2md5(filename)
2184
2185 if original_md5 == modified_md5:
2186 _log.debug('visual progress note (template) not modified')
2187
2188 msg = _(
2189 u'You either created a visual progress note from a template\n'
2190 u'in the database (rather than from a file on disk) or you\n'
2191 u'edited an existing visual progress note.\n'
2192 u'\n'
2193 u'The template/original was not modified at all, however.\n'
2194 u'\n'
2195 u'Do you still want to save the unmodified image as a\n'
2196 u'visual progress note into the EMR of the patient ?\n'
2197 )
2198 save_unmodified = gmGuiHelpers.gm_show_question (
2199 msg,
2200 _('Saving visual progress note')
2201 )
2202 if not save_unmodified:
2203 _log.debug('user discarded unmodified note')
2204 return
2205
2206 if doc_part is not None:
2207 doc_part.update_data_from_file(fname = filename)
2208 doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2209 return None
2210
2211 if not isinstance(episode, gmEMRStructItems.cEpisode):
2212 if episode is None:
2213 episode = _('visual progress notes')
2214 pat = gmPerson.gmCurrentPatient()
2215 emr = pat.get_emr()
2216 episode = emr.add_episode(episode_name = episode.strip(), pk_health_issue = health_issue, is_open = False)
2217
2218 doc = gmDocumentWidgets.save_file_as_new_document (
2219 filename = filename,
2220 document_type = gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE,
2221 episode = episode,
2222 unlock_patient = True
2223 )
2224 doc.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2225
2226 return doc
2227
2229 """Phrasewheel to allow selection of visual SOAP template."""
2230
2232
2233 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
2234
2235 query = u"""
2236 SELECT
2237 pk AS data,
2238 name_short AS list_label,
2239 name_sort AS field_label
2240 FROM
2241 ref.paperwork_templates
2242 WHERE
2243 fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND (
2244 name_long %%(fragment_condition)s
2245 OR
2246 name_short %%(fragment_condition)s
2247 )
2248 ORDER BY list_label
2249 LIMIT 15
2250 """ % gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE
2251
2252 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
2253 mp.setThresholds(2, 3, 5)
2254
2255 self.matcher = mp
2256 self.selection_only = True
2257
2263
2264 from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl
2265
2267
2272
2273
2274
2275 - def refresh(self, document_folder=None, episodes=None, encounter=None):
2276
2277 self.clear()
2278 if document_folder is not None:
2279 soap_docs = document_folder.get_visual_progress_notes(episodes = episodes, encounter = encounter)
2280 if len(soap_docs) > 0:
2281 for soap_doc in soap_docs:
2282 parts = soap_doc.parts
2283 if len(parts) == 0:
2284 continue
2285 part = parts[0]
2286 fname = part.export_to_file()
2287 if fname is None:
2288 continue
2289
2290
2291 img = gmGuiHelpers.file2scaled_image (
2292 filename = fname,
2293 height = 30
2294 )
2295
2296 bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER)
2297
2298
2299 img = gmGuiHelpers.file2scaled_image (
2300 filename = fname,
2301 height = 150
2302 )
2303 tip = agw_stt.SuperToolTip (
2304 u'',
2305 bodyImage = img,
2306 header = _('Created: %s') % part['date_generated'].strftime('%Y %B %d').decode(gmI18N.get_encoding()),
2307 footer = gmTools.coalesce(part['doc_comment'], u'').strip()
2308 )
2309 tip.SetTopGradientColor('white')
2310 tip.SetMiddleGradientColor('white')
2311 tip.SetBottomGradientColor('white')
2312 tip.SetTarget(bmp)
2313
2314 bmp.doc_part = part
2315 bmp.Bind(wx.EVT_LEFT_UP, self._on_bitmap_leftclicked)
2316
2317 self._SZR_soap.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM | wx.EXPAND, 3)
2318 self.__bitmaps.append(bmp)
2319
2320 self.GetParent().Layout()
2321
2323 while len(self._SZR_soap.GetChildren()) > 0:
2324 self._SZR_soap.Detach(0)
2325
2326
2327 for bmp in self.__bitmaps:
2328 bmp.Destroy()
2329 self.__bitmaps = []
2330
2332 wx.CallAfter (
2333 edit_visual_progress_note,
2334 doc_part = evt.GetEventObject().doc_part,
2335 discard_unmodified = True
2336 )
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578 if __name__ == '__main__':
2579
2580 if len(sys.argv) < 2:
2581 sys.exit()
2582
2583 if sys.argv[1] != 'test':
2584 sys.exit()
2585
2586 gmI18N.activate_locale()
2587 gmI18N.install_domain(domain = 'gnumed')
2588
2589
2598
2605
2618
2619
2620 test_cSoapNoteExpandoEditAreaPnl()
2621
2622
2623
2624