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'soapu',
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'soapu')) ])
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'soapu')) ])
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 = 'soapu',
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 = 'soapu',
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
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 if expando.NumberOfLines == 0:
1747 no_of_lines = 1
1748 else:
1749 no_of_lines = expando.NumberOfLines
1750 y_cursor = int(round((float(line_cursor) / no_of_lines) * h_expando))
1751 y_desired_visible = y_expando + y_cursor
1752
1753 y_view = self.ViewStart[1]
1754 h_view = self.GetClientSizeTuple()[1]
1755
1756
1757
1758
1759
1760
1761
1762
1763 if y_desired_visible < y_view:
1764
1765 self.Scroll(0, y_desired_visible)
1766
1767 if y_desired_visible > h_view:
1768
1769 self.Scroll(0, y_desired_visible)
1770
1771
1772
1774 soap_notes = []
1775
1776 tmp = self._TCTRL_Soap.GetValue().strip()
1777 if tmp != u'':
1778 soap_notes.append(['s', tmp])
1779
1780 tmp = self._TCTRL_sOap.GetValue().strip()
1781 if tmp != u'':
1782 soap_notes.append(['o', tmp])
1783
1784 tmp = self._TCTRL_soAp.GetValue().strip()
1785 if tmp != u'':
1786 soap_notes.append(['a', tmp])
1787
1788 tmp = self._TCTRL_soaP.GetValue().strip()
1789 if tmp != u'':
1790 soap_notes.append(['p', tmp])
1791
1792 return soap_notes
1793
1794 soap = property(_get_soap, lambda x:x)
1795
1797
1798
1799 for field in self.soap_fields:
1800 if field.GetValue().strip() != u'':
1801 return False
1802
1803
1804 summary = self._TCTRL_episode_summary.GetValue().strip()
1805 if self.problem is None:
1806 if summary != u'':
1807 return False
1808 elif self.problem['type'] == u'issue':
1809 if summary != u'':
1810 return False
1811 else:
1812 if self.problem['summary'] is None:
1813 if summary != u'':
1814 return False
1815 else:
1816 if summary != self.problem['summary'].strip():
1817 return False
1818
1819
1820 new_codes = self._PRW_episode_codes.GetData()
1821 if self.problem is None:
1822 if len(new_codes) > 0:
1823 return False
1824 elif self.problem['type'] == u'issue':
1825 if len(new_codes) > 0:
1826 return False
1827 else:
1828 old_code_pks = self.problem.generic_codes
1829 if len(old_code_pks) != len(new_codes):
1830 return False
1831 for code in new_codes:
1832 if code['data'] not in old_code_pks:
1833 return False
1834
1835 return True
1836
1837 empty = property(_get_empty, lambda x:x)
1838
1839 -class cSoapLineTextCtrl(wx_expando.ExpandoTextCtrl):
1840
1841 - def __init__(self, *args, **kwargs):
1842
1843 wx_expando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
1844
1845 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+")
1846
1847 self.__register_interests()
1848
1849
1850
1851 - def _wrapLine(self, line, dc, width):
1852
1853 if (wx.MAJOR_VERSION >= 2) and (wx.MINOR_VERSION > 8):
1854 return super(cSoapLineTextCtrl, self)._wrapLine(line, dc, width)
1855
1856
1857
1858
1859 pte = dc.GetPartialTextExtents(line)
1860 width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X)
1861 idx = 0
1862 start = 0
1863 count = 0
1864 spc = -1
1865 while idx < len(pte):
1866 if line[idx] == ' ':
1867 spc = idx
1868 if pte[idx] - start > width:
1869
1870 count += 1
1871
1872 if spc != -1:
1873 idx = spc + 1
1874 spc = -1
1875 if idx < len(pte):
1876 start = pte[idx]
1877 else:
1878 idx += 1
1879 return count
1880
1881
1882
1884
1885
1886 wx.EVT_CHAR(self, self.__on_char)
1887 wx.EVT_SET_FOCUS(self, self.__on_focus)
1888
1889 - def __on_focus(self, evt):
1890 evt.Skip()
1891 wx.CallAfter(self._after_on_focus)
1892
1893 - def _after_on_focus(self):
1894
1895 evt = wx.PyCommandEvent(wx_expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
1896 evt.SetEventObject(self)
1897
1898
1899
1900
1901 self.GetEventHandler().ProcessEvent(evt)
1902
1903 - def __on_char(self, evt):
1904 char = unichr(evt.GetUnicodeKey())
1905
1906 if self.LastPosition == 1:
1907 evt.Skip()
1908 return
1909
1910 explicit_expansion = False
1911 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT):
1912 if evt.GetKeyCode() != 13:
1913 evt.Skip()
1914 return
1915 explicit_expansion = True
1916
1917 if not explicit_expansion:
1918 if self.__keyword_separators.match(char) is None:
1919 evt.Skip()
1920 return
1921
1922 caret_pos, line_no = self.PositionToXY(self.InsertionPoint)
1923 line = self.GetLineText(line_no)
1924 word = self.__keyword_separators.split(line[:caret_pos])[-1]
1925
1926 if (
1927 (not explicit_expansion)
1928 and
1929 (word != u'$$steffi')
1930 and
1931 (word not in [ r[0] for r in gmPG2.get_text_expansion_keywords() ])
1932 ):
1933 evt.Skip()
1934 return
1935
1936 start = self.InsertionPoint - len(word)
1937 wx.CallAfter(self.replace_keyword_with_expansion, word, start, explicit_expansion)
1938
1939 evt.Skip()
1940 return
1941
1942 - def replace_keyword_with_expansion(self, keyword=None, position=None, show_list=False):
1943
1944 if show_list:
1945 candidates = gmPG2.get_keyword_expansion_candidates(keyword = keyword)
1946 if len(candidates) == 0:
1947 return
1948 if len(candidates) == 1:
1949 keyword = candidates[0]
1950 else:
1951 keyword = gmListWidgets.get_choices_from_list (
1952 parent = self,
1953 msg = _(
1954 'Several macros match the keyword [%s].\n'
1955 '\n'
1956 'Please select the expansion you want to happen.'
1957 ) % keyword,
1958 caption = _('Selecting text macro'),
1959 choices = candidates,
1960 columns = [_('Keyword')],
1961 single_selection = True,
1962 can_return_empty = False
1963 )
1964 if keyword is None:
1965 return
1966
1967 expansion = gmPG2.expand_keyword(keyword = keyword)
1968
1969 if expansion is None:
1970 return
1971
1972 if expansion == u'':
1973 return
1974
1975 self.Replace (
1976 position,
1977 position + len(keyword),
1978 expansion
1979 )
1980
1981 self.SetInsertionPoint(position + len(expansion) + 1)
1982 self.ShowPosition(position + len(expansion) + 1)
1983
1984 return
1985
1986
1987
2018
2019 cmd = gmCfgWidgets.configure_string_option (
2020 message = _(
2021 'Enter the shell command with which to start\n'
2022 'the image editor for visual progress notes.\n'
2023 '\n'
2024 'Any "%(img)s" included with the arguments\n'
2025 'will be replaced by the file name of the\n'
2026 'note template.'
2027 ),
2028 option = u'external.tools.visual_soap_editor_cmd',
2029 bias = 'user',
2030 default_value = None,
2031 validator = is_valid
2032 )
2033
2034 return cmd
2035
2037 if parent is None:
2038 parent = wx.GetApp().GetTopWindow()
2039
2040 dlg = wx.FileDialog (
2041 parent = parent,
2042 message = _('Choose file to use as template for new visual progress note'),
2043 defaultDir = os.path.expanduser('~'),
2044 defaultFile = '',
2045
2046 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST
2047 )
2048 result = dlg.ShowModal()
2049
2050 if result == wx.ID_CANCEL:
2051 dlg.Destroy()
2052 return None
2053
2054 full_filename = dlg.GetPath()
2055 dlg.Hide()
2056 dlg.Destroy()
2057 return full_filename
2058
2060
2061 if parent is None:
2062 parent = wx.GetApp().GetTopWindow()
2063
2064 dlg = gmGuiHelpers.c3ButtonQuestionDlg (
2065 parent,
2066 -1,
2067 caption = _('Visual progress note source'),
2068 question = _('From which source do you want to pick the image template ?'),
2069 button_defs = [
2070 {'label': _('Database'), 'tooltip': _('List of templates in the database.'), 'default': True},
2071 {'label': _('File'), 'tooltip': _('Files in the filesystem.'), 'default': False},
2072 {'label': _('Device'), 'tooltip': _('Image capture devices (scanners, cameras, etc)'), 'default': False}
2073 ]
2074 )
2075 result = dlg.ShowModal()
2076 dlg.Destroy()
2077
2078
2079 if result == wx.ID_YES:
2080 _log.debug('visual progress note template from: database template')
2081 from Gnumed.wxpython import gmFormWidgets
2082 template = gmFormWidgets.manage_form_templates (
2083 parent = parent,
2084 template_types = [gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE],
2085 active_only = True
2086 )
2087 if template is None:
2088 return (None, None)
2089 filename = template.export_to_file()
2090 if filename is None:
2091 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note template for [%s].') % template['name_long'])
2092 return (None, None)
2093 return (filename, True)
2094
2095
2096 if result == wx.ID_NO:
2097 _log.debug('visual progress note template from: disk file')
2098 fname = select_file_as_visual_progress_note_template(parent = parent)
2099 if fname is None:
2100 return (None, None)
2101
2102 ext = os.path.splitext(fname)[1]
2103 tmp_name = gmTools.get_unique_filename(suffix = ext)
2104 _log.debug('visual progress note from file: [%s] -> [%s]', fname, tmp_name)
2105 shutil.copy2(fname, tmp_name)
2106 return (tmp_name, False)
2107
2108
2109 if result == wx.ID_CANCEL:
2110 _log.debug('visual progress note template from: image capture device')
2111 fnames = gmDocumentWidgets.acquire_images_from_capture_device(device = None, calling_window = parent)
2112 if fnames is None:
2113 return (None, None)
2114 if len(fnames) == 0:
2115 return (None, None)
2116 return (fnames[0], False)
2117
2118 _log.debug('no visual progress note template source selected')
2119 return (None, None)
2120
2122 """This assumes <filename> contains an image which can be handled by the configured image editor."""
2123
2124 if doc_part is not None:
2125 filename = doc_part.export_to_file()
2126 if filename is None:
2127 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.'))
2128 return None
2129
2130 dbcfg = gmCfg.cCfgSQL()
2131 cmd = dbcfg.get2 (
2132 option = u'external.tools.visual_soap_editor_cmd',
2133 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2134 bias = 'user'
2135 )
2136
2137 if cmd is None:
2138 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = False)
2139 cmd = configure_visual_progress_note_editor()
2140 if cmd is None:
2141 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = True)
2142 return None
2143
2144 if u'%(img)s' in cmd:
2145 cmd % {u'img': filename}
2146 else:
2147 cmd = u'%s %s' % (cmd, filename)
2148
2149 if discard_unmodified:
2150 original_stat = os.stat(filename)
2151 original_md5 = gmTools.file2md5(filename)
2152
2153 success = gmShellAPI.run_command_in_shell(cmd, blocking = True)
2154 if not success:
2155 gmGuiHelpers.gm_show_error (
2156 _(
2157 'There was a problem with running the editor\n'
2158 'for visual progress notes.\n'
2159 '\n'
2160 ' [%s]\n'
2161 '\n'
2162 ) % cmd,
2163 _('Editing visual progress note')
2164 )
2165 return None
2166
2167 try:
2168 open(filename, 'r').close()
2169 except StandardError:
2170 _log.exception('problem accessing visual progress note file [%s]', filename)
2171 gmGuiHelpers.gm_show_error (
2172 _(
2173 'There was a problem reading the visual\n'
2174 'progress note from the file:\n'
2175 '\n'
2176 ' [%s]\n'
2177 '\n'
2178 ) % filename,
2179 _('Saving visual progress note')
2180 )
2181 return None
2182
2183 if discard_unmodified:
2184 modified_stat = os.stat(filename)
2185
2186 if original_stat.st_size == modified_stat.st_size:
2187 modified_md5 = gmTools.file2md5(filename)
2188
2189 if original_md5 == modified_md5:
2190 _log.debug('visual progress note (template) not modified')
2191
2192 msg = _(
2193 u'You either created a visual progress note from a template\n'
2194 u'in the database (rather than from a file on disk) or you\n'
2195 u'edited an existing visual progress note.\n'
2196 u'\n'
2197 u'The template/original was not modified at all, however.\n'
2198 u'\n'
2199 u'Do you still want to save the unmodified image as a\n'
2200 u'visual progress note into the EMR of the patient ?\n'
2201 )
2202 save_unmodified = gmGuiHelpers.gm_show_question (
2203 msg,
2204 _('Saving visual progress note')
2205 )
2206 if not save_unmodified:
2207 _log.debug('user discarded unmodified note')
2208 return
2209
2210 if doc_part is not None:
2211 doc_part.update_data_from_file(fname = filename)
2212 doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2213 return None
2214
2215 if not isinstance(episode, gmEMRStructItems.cEpisode):
2216 if episode is None:
2217 episode = _('visual progress notes')
2218 pat = gmPerson.gmCurrentPatient()
2219 emr = pat.get_emr()
2220 episode = emr.add_episode(episode_name = episode.strip(), pk_health_issue = health_issue, is_open = False)
2221
2222 doc = gmDocumentWidgets.save_file_as_new_document (
2223 filename = filename,
2224 document_type = gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE,
2225 episode = episode,
2226 unlock_patient = True
2227 )
2228 doc.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2229
2230 return doc
2231
2233 """Phrasewheel to allow selection of visual SOAP template."""
2234
2236
2237 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
2238
2239 query = u"""
2240 SELECT
2241 pk AS data,
2242 name_short AS list_label,
2243 name_sort AS field_label
2244 FROM
2245 ref.paperwork_templates
2246 WHERE
2247 fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND (
2248 name_long %%(fragment_condition)s
2249 OR
2250 name_short %%(fragment_condition)s
2251 )
2252 ORDER BY list_label
2253 LIMIT 15
2254 """ % gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE
2255
2256 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
2257 mp.setThresholds(2, 3, 5)
2258
2259 self.matcher = mp
2260 self.selection_only = True
2261
2267
2268 from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl
2269
2271
2276
2277
2278
2279 - def refresh(self, document_folder=None, episodes=None, encounter=None):
2280
2281 self.clear()
2282 if document_folder is not None:
2283 soap_docs = document_folder.get_visual_progress_notes(episodes = episodes, encounter = encounter)
2284 if len(soap_docs) > 0:
2285 for soap_doc in soap_docs:
2286 parts = soap_doc.parts
2287 if len(parts) == 0:
2288 continue
2289 part = parts[0]
2290 fname = part.export_to_file()
2291 if fname is None:
2292 continue
2293
2294
2295 img = gmGuiHelpers.file2scaled_image (
2296 filename = fname,
2297 height = 30
2298 )
2299
2300 bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER)
2301
2302
2303 img = gmGuiHelpers.file2scaled_image (
2304 filename = fname,
2305 height = 150
2306 )
2307 tip = agw_stt.SuperToolTip (
2308 u'',
2309 bodyImage = img,
2310 header = _('Created: %s') % part['date_generated'].strftime('%Y %B %d').decode(gmI18N.get_encoding()),
2311 footer = gmTools.coalesce(part['doc_comment'], u'').strip()
2312 )
2313 tip.SetTopGradientColor('white')
2314 tip.SetMiddleGradientColor('white')
2315 tip.SetBottomGradientColor('white')
2316 tip.SetTarget(bmp)
2317
2318 bmp.doc_part = part
2319 bmp.Bind(wx.EVT_LEFT_UP, self._on_bitmap_leftclicked)
2320
2321 self._SZR_soap.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM | wx.EXPAND, 3)
2322 self.__bitmaps.append(bmp)
2323
2324 self.GetParent().Layout()
2325
2327 while len(self._SZR_soap.GetChildren()) > 0:
2328 self._SZR_soap.Detach(0)
2329
2330
2331 for bmp in self.__bitmaps:
2332 bmp.Destroy()
2333 self.__bitmaps = []
2334
2336 wx.CallAfter (
2337 edit_visual_progress_note,
2338 doc_part = evt.GetEventObject().doc_part,
2339 discard_unmodified = True
2340 )
2341
2342 from Gnumed.wxGladeWidgets import wxgSimpleSoapPluginPnl
2343
2344 -class cSimpleSoapPluginPnl(wxgSimpleSoapPluginPnl.wxgSimpleSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
2354
2355
2356
2358 self._LCTRL_problems.set_columns(columns = [_('Problem list')])
2359 self._LCTRL_problems.activate_callback = self._on_problem_activated
2360 self._LCTRL_problems.item_tooltip_callback = self._on_get_problem_tooltip
2361
2362 self._splitter_main.SetSashGravity(0.5)
2363 splitter_width = self._splitter_main.GetSizeTuple()[0]
2364 self._splitter_main.SetSashPosition(splitter_width / 2, True)
2365
2367 self._LCTRL_problems.set_string_items()
2368 self._TCTRL_soap_problem.SetValue(u'')
2369 self._TCTRL_soap.SetValue(u'')
2370 self._CHBOX_filter_by_problem.SetLabel(_('&Filter by problem'))
2371 self._TCTRL_journal.SetValue(u'')
2372
2374 if not self.__pat.connected:
2375 return None
2376
2377 if self.__problem is None:
2378 return None
2379
2380 saved = self.__pat.emr.add_clin_narrative (
2381 note = self._TCTRL_soap.GetValue().strip(),
2382 soap_cat = u'u',
2383 episode = self.__problem
2384 )
2385
2386 if saved is None:
2387 return False
2388
2389 self._TCTRL_soap.SetValue(u'')
2390 self.__refresh_journal()
2391 return True
2392
2394 if self._TCTRL_soap.GetValue().strip() == u'':
2395 return True
2396 save_it = gmGuiHelpers.gm_show_question (
2397 title = _('Saving SOAP note'),
2398 question = _('Do you want to save the SOAP note ?')
2399 )
2400 if save_it:
2401 return self.__save_soap()
2402 return False
2403
2414
2435
2436
2437
2439 """Configure enabled event signals."""
2440
2441 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
2442 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
2443 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db)
2444 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
2445
2446
2447 self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback)
2448 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
2449
2451 """Another patient is about to be activated.
2452
2453 Patient change will not proceed before this returns True.
2454 """
2455 if not self.__pat.connected:
2456 return True
2457 self.__perhaps_save_soap()
2458 self.__problem = None
2459 return True
2460
2462 """The client is about to be shut down.
2463
2464 Shutdown will not proceed before this returns.
2465 """
2466 if not self.__pat.connected:
2467 return
2468 if not self.__save_soap():
2469 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save SimpleNotes SOAP note.'), beep = True)
2470 return
2471
2473 wx.CallAfter(self.__reset_ui)
2474
2476 wx.CallAfter(self._schedule_data_reget)
2477
2479 wx.CallAfter(self._schedule_data_reget)
2480
2482 self.__perhaps_save_soap()
2483 epi = self._LCTRL_problems.get_selected_item_data(only_one = True)
2484 self._TCTRL_soap_problem.SetValue(_('Progress note: %s%s') % (
2485 epi['description'],
2486 gmTools.coalesce(epi['health_issue'], u'', u' (%s)')
2487 ))
2488 self.__problem = epi
2489 self._TCTRL_soap.SetValue(u'')
2490
2505
2507 event.Skip()
2508 self.__refresh_journal()
2509
2511 event.Skip()
2512 self.__refresh_journal()
2513
2528
2535
2543
2547
2551
2552
2553
2555 self.__refresh_problem_list()
2556 self.__refresh_journal()
2557 self._TCTRL_soap.SetValue(u'')
2558 return True
2559
2560
2561
2562
2563 if __name__ == '__main__':
2564
2565 if len(sys.argv) < 2:
2566 sys.exit()
2567
2568 if sys.argv[1] != 'test':
2569 sys.exit()
2570
2571 gmI18N.activate_locale()
2572 gmI18N.install_domain(domain = 'gnumed')
2573
2574
2583
2590
2603
2604
2605 test_cSoapNoteExpandoEditAreaPnl()
2606
2607
2608
2609