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