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 = [ gmPerson.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 ) % gmPerson.gmCurrentProvider()['short_alias'],
220 columns = [_('when'), _('type'), _('entry')],
221 single_selection = True,
222 can_return_empty = False,
223 edit_callback = edit,
224 delete_callback = delete,
225 refresh_callback = refresh,
226 ignore_OK_button = True
227 )
228
230
231 if parent is None:
232 parent = wx.GetApp().GetTopWindow()
233
234 searcher = wx.TextEntryDialog (
235 parent = parent,
236 message = _('Enter (regex) term to search for across all EMRs:'),
237 caption = _('Text search across all EMRs'),
238 style = wx.OK | wx.CANCEL | wx.CENTRE
239 )
240 result = searcher.ShowModal()
241
242 if result != wx.ID_OK:
243 return
244
245 wx.BeginBusyCursor()
246 term = searcher.GetValue()
247 searcher.Destroy()
248 results = gmClinNarrative.search_text_across_emrs(search_term = term)
249 wx.EndBusyCursor()
250
251 if len(results) == 0:
252 gmGuiHelpers.gm_show_info (
253 _(
254 'Nothing found for search term:\n'
255 ' "%s"'
256 ) % term,
257 _('Search results')
258 )
259 return
260
261 items = [ [gmPerson.cIdentity(aPK_obj =
262 r['pk_patient'])['description_gender'], r['narrative'],
263 r['src_table']] for r in results ]
264
265 selected_patient = gmListWidgets.get_choices_from_list (
266 parent = parent,
267 caption = _('Search results for %s') % term,
268 choices = items,
269 columns = [_('Patient'), _('Match'), _('Match location')],
270 data = [ r['pk_patient'] for r in results ],
271 single_selection = True,
272 can_return_empty = False
273 )
274
275 if selected_patient is None:
276 return
277
278 wx.CallAfter(gmPatSearchWidgets.set_active_patient, patient = gmPerson.cIdentity(aPK_obj = selected_patient))
279
281
282
283 if patient is None:
284 patient = gmPerson.gmCurrentPatient()
285
286 if not patient.connected:
287 gmDispatcher.send(signal = 'statustext', msg = _('Cannot search EMR. No active patient.'))
288 return False
289
290 if parent is None:
291 parent = wx.GetApp().GetTopWindow()
292
293 searcher = wx.TextEntryDialog (
294 parent = parent,
295 message = _('Enter search term:'),
296 caption = _('Text search of entire EMR of active patient'),
297 style = wx.OK | wx.CANCEL | wx.CENTRE
298 )
299 result = searcher.ShowModal()
300
301 if result != wx.ID_OK:
302 searcher.Destroy()
303 return False
304
305 wx.BeginBusyCursor()
306 val = searcher.GetValue()
307 searcher.Destroy()
308 emr = patient.get_emr()
309 rows = emr.search_narrative_simple(val)
310 wx.EndBusyCursor()
311
312 if len(rows) == 0:
313 gmGuiHelpers.gm_show_info (
314 _(
315 'Nothing found for search term:\n'
316 ' "%s"'
317 ) % val,
318 _('Search results')
319 )
320 return True
321
322 txt = u''
323 for row in rows:
324 txt += u'%s: %s\n' % (
325 row['soap_cat'],
326 row['narrative']
327 )
328
329 txt += u' %s: %s - %s %s\n' % (
330 _('Encounter'),
331 row['encounter_started'].strftime('%x %H:%M'),
332 row['encounter_ended'].strftime('%H:%M'),
333 row['encounter_type']
334 )
335 txt += u' %s: %s\n' % (
336 _('Episode'),
337 row['episode']
338 )
339 txt += u' %s: %s\n\n' % (
340 _('Health issue'),
341 row['health_issue']
342 )
343
344 msg = _(
345 'Search term was: "%s"\n'
346 '\n'
347 'Search results:\n\n'
348 '%s\n'
349 ) % (val, txt)
350
351 dlg = wx.MessageDialog (
352 parent = parent,
353 message = msg,
354 caption = _('Search results for %s') % val,
355 style = wx.OK | wx.STAY_ON_TOP
356 )
357 dlg.ShowModal()
358 dlg.Destroy()
359
360 return True
361
363
364
365 pat = gmPerson.gmCurrentPatient()
366 if not pat.connected:
367 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR for Medistar. No active patient.'))
368 return False
369
370 if encounter is None:
371 encounter = pat.get_emr().active_encounter
372
373 if parent is None:
374 parent = wx.GetApp().GetTopWindow()
375
376
377 aWildcard = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files"))
378
379 aDefDir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed','export')))
380
381 fname = '%s-%s-%s-%s-%s.txt' % (
382 'Medistar-MD',
383 time.strftime('%Y-%m-%d',time.localtime()),
384 pat['lastnames'].replace(' ', '-'),
385 pat['firstnames'].replace(' ', '_'),
386 pat.get_formatted_dob(format = '%Y-%m-%d')
387 )
388 dlg = wx.FileDialog (
389 parent = parent,
390 message = _("Save EMR extract for MEDISTAR import as..."),
391 defaultDir = aDefDir,
392 defaultFile = fname,
393 wildcard = aWildcard,
394 style = wx.SAVE
395 )
396 choice = dlg.ShowModal()
397 fname = dlg.GetPath()
398 dlg.Destroy()
399 if choice != wx.ID_OK:
400 return False
401
402 wx.BeginBusyCursor()
403 _log.debug('exporting encounter for medistar import to [%s]', fname)
404 exporter = gmPatientExporter.cMedistarSOAPExporter()
405 successful, fname = exporter.export_to_file (
406 filename = fname,
407 encounter = encounter,
408 soap_cats = u'soap',
409 export_to_import_file = True
410 )
411 if not successful:
412 gmGuiHelpers.gm_show_error (
413 _('Error exporting progress notes for MEDISTAR import.'),
414 _('MEDISTAR progress notes export')
415 )
416 wx.EndBusyCursor()
417 return False
418
419 gmDispatcher.send(signal = 'statustext', msg = _('Successfully exported progress notes into file [%s] for Medistar import.') % fname, beep=False)
420
421 wx.EndBusyCursor()
422 return True
423
425 """soap_cats needs to be a list"""
426
427 if parent is None:
428 parent = wx.GetApp().GetTopWindow()
429
430 pat = gmPerson.gmCurrentPatient()
431 emr = pat.get_emr()
432
433 selected_soap = {}
434 selected_narrative_pks = []
435
436
437 def pick_soap_from_episode(episode):
438
439 narr_for_epi = emr.get_clin_narrative(episodes = [episode['pk_episode']], soap_cats = soap_cats)
440
441 if len(narr_for_epi) == 0:
442 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episode.'))
443 return True
444
445 dlg = cNarrativeListSelectorDlg (
446 parent = parent,
447 id = -1,
448 narrative = narr_for_epi,
449 msg = _(
450 '\n This is the narrative (type %s) for the chosen episodes.\n'
451 '\n'
452 ' Now, mark the entries you want to include in your report.\n'
453 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soap')) ])
454 )
455
456
457
458
459
460
461 btn_pressed = dlg.ShowModal()
462 selected_narr = dlg.get_selected_item_data()
463 dlg.Destroy()
464
465 if btn_pressed == wx.ID_CANCEL:
466 return True
467
468 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ]
469 for narr in selected_narr:
470 selected_soap[narr['pk_narrative']] = narr
471
472 print "before returning from picking soap"
473
474 return True
475
476 selected_episode_pks = []
477
478 all_epis = [ epi for epi in emr.get_episodes() if epi.has_narrative ]
479
480 if len(all_epis) == 0:
481 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.'))
482 return []
483
484 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg (
485 parent = parent,
486 id = -1,
487 episodes = all_epis,
488 msg = _('\n Select the the episode you want to report on.\n')
489 )
490
491
492
493
494
495
496 dlg.left_extra_button = (
497 _('Pick SOAP'),
498 _('Pick SOAP entries from topmost selected episode'),
499 pick_soap_from_episode
500 )
501 btn_pressed = dlg.ShowModal()
502 dlg.Destroy()
503
504 if btn_pressed == wx.ID_CANCEL:
505 return None
506
507 return selected_soap.values()
508
510 """soap_cats needs to be a list"""
511
512 pat = gmPerson.gmCurrentPatient()
513 emr = pat.get_emr()
514
515 if parent is None:
516 parent = wx.GetApp().GetTopWindow()
517
518 selected_soap = {}
519 selected_issue_pks = []
520 selected_episode_pks = []
521 selected_narrative_pks = []
522
523 while 1:
524
525 all_issues = emr.get_health_issues()
526 all_issues.insert(0, gmEMRStructItems.get_dummy_health_issue())
527 dlg = gmEMRStructWidgets.cIssueListSelectorDlg (
528 parent = parent,
529 id = -1,
530 issues = all_issues,
531 msg = _('\n In the list below mark the health issues you want to report on.\n')
532 )
533 selection_idxs = []
534 for idx in range(len(all_issues)):
535 if all_issues[idx]['pk_health_issue'] in selected_issue_pks:
536 selection_idxs.append(idx)
537 if len(selection_idxs) != 0:
538 dlg.set_selections(selections = selection_idxs)
539 btn_pressed = dlg.ShowModal()
540 selected_issues = dlg.get_selected_item_data()
541 dlg.Destroy()
542
543 if btn_pressed == wx.ID_CANCEL:
544 return selected_soap.values()
545
546 selected_issue_pks = [ i['pk_health_issue'] for i in selected_issues ]
547
548 while 1:
549
550 all_epis = emr.get_episodes(issues = selected_issue_pks)
551
552 if len(all_epis) == 0:
553 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.'))
554 break
555
556 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg (
557 parent = parent,
558 id = -1,
559 episodes = all_epis,
560 msg = _(
561 '\n These are the episodes known for the health issues just selected.\n\n'
562 ' Now, mark the the episodes you want to report on.\n'
563 )
564 )
565 selection_idxs = []
566 for idx in range(len(all_epis)):
567 if all_epis[idx]['pk_episode'] in selected_episode_pks:
568 selection_idxs.append(idx)
569 if len(selection_idxs) != 0:
570 dlg.set_selections(selections = selection_idxs)
571 btn_pressed = dlg.ShowModal()
572 selected_epis = dlg.get_selected_item_data()
573 dlg.Destroy()
574
575 if btn_pressed == wx.ID_CANCEL:
576 break
577
578 selected_episode_pks = [ i['pk_episode'] for i in selected_epis ]
579
580
581 all_narr = emr.get_clin_narrative(episodes = selected_episode_pks, soap_cats = soap_cats)
582
583 if len(all_narr) == 0:
584 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episodes.'))
585 continue
586
587 dlg = cNarrativeListSelectorDlg (
588 parent = parent,
589 id = -1,
590 narrative = all_narr,
591 msg = _(
592 '\n This is the narrative (type %s) for the chosen episodes.\n\n'
593 ' Now, mark the entries you want to include in your report.\n'
594 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soap')) ])
595 )
596 selection_idxs = []
597 for idx in range(len(all_narr)):
598 if all_narr[idx]['pk_narrative'] in selected_narrative_pks:
599 selection_idxs.append(idx)
600 if len(selection_idxs) != 0:
601 dlg.set_selections(selections = selection_idxs)
602 btn_pressed = dlg.ShowModal()
603 selected_narr = dlg.get_selected_item_data()
604 dlg.Destroy()
605
606 if btn_pressed == wx.ID_CANCEL:
607 continue
608
609 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ]
610 for narr in selected_narr:
611 selected_soap[narr['pk_narrative']] = narr
612
614
616
617 narrative = kwargs['narrative']
618 del kwargs['narrative']
619
620 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs)
621
622 self.SetTitle(_('Select the narrative you are interested in ...'))
623
624 self._LCTRL_items.set_columns([_('when'), _('who'), _('type'), _('entry')])
625
626 self._LCTRL_items.set_string_items (
627 items = [ [narr['date'].strftime('%x %H:%M'), narr['provider'], gmClinNarrative.soap_cat2l10n[narr['soap_cat']], narr['narrative'].replace('\n', '/').replace('\r', '/')] for narr in narrative ]
628 )
629 self._LCTRL_items.set_column_widths()
630 self._LCTRL_items.set_data(data = narrative)
631
632 from Gnumed.wxGladeWidgets import wxgMoveNarrativeDlg
633
635
637
638 self.encounter = kwargs['encounter']
639 self.source_episode = kwargs['episode']
640 del kwargs['encounter']
641 del kwargs['episode']
642
643 wxgMoveNarrativeDlg.wxgMoveNarrativeDlg.__init__(self, *args, **kwargs)
644
645 self.LBL_source_episode.SetLabel(u'%s%s' % (self.source_episode['description'], gmTools.coalesce(self.source_episode['health_issue'], u'', u' (%s)')))
646 self.LBL_encounter.SetLabel('%s: %s %s - %s' % (
647 self.encounter['started'].strftime('%x').decode(gmI18N.get_encoding()),
648 self.encounter['l10n_type'],
649 self.encounter['started'].strftime('%H:%M'),
650 self.encounter['last_affirmed'].strftime('%H:%M')
651 ))
652 pat = gmPerson.gmCurrentPatient()
653 emr = pat.get_emr()
654 narr = emr.get_clin_narrative(episodes=[self.source_episode['pk_episode']], encounters=[self.encounter['pk_encounter']])
655 if len(narr) == 0:
656 narr = [{'narrative': _('There is no narrative for this episode in this encounter.')}]
657 self.LBL_narrative.SetLabel(u'\n'.join([n['narrative'] for n in narr]))
658
659
681
682 from Gnumed.wxGladeWidgets import wxgSoapPluginPnl
683
684 -class cSoapPluginPnl(wxgSoapPluginPnl.wxgSoapPluginPnl, gmRegetMixin.cRegetOnPaintMixin):
685 """A panel for in-context editing of progress notes.
686
687 Expects to be used as a notebook page.
688
689 Left hand side:
690 - problem list (health issues and active episodes)
691 - previous notes
692
693 Right hand side:
694 - encounter details fields
695 - notebook with progress note editors
696 - visual progress notes
697 - hints
698
699 Listens to patient change signals, thus acts on the current patient.
700 """
712
713
714
716
717 if not self.__encounter_valid_for_save():
718 return False
719
720 emr = self.__pat.get_emr()
721 enc = emr.active_encounter
722
723 enc['pk_type'] = self._PRW_encounter_type.GetData()
724 enc['started'] = self._PRW_encounter_start.GetData().get_pydt()
725 enc['last_affirmed'] = self._PRW_encounter_end.GetData().get_pydt()
726 rfe = self._TCTRL_rfe.GetValue().strip()
727 if len(rfe) == 0:
728 enc['reason_for_encounter'] = None
729 else:
730 enc['reason_for_encounter'] = rfe
731 aoe = self._TCTRL_aoe.GetValue().strip()
732 if len(aoe) == 0:
733 enc['assessment_of_encounter'] = None
734 else:
735 enc['assessment_of_encounter'] = aoe
736
737 enc.save_payload()
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 """Clear all information from input panel."""
761
762 self._LCTRL_active_problems.set_string_items()
763
764 self._TCTRL_recent_notes.SetValue(u'')
765 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on selected problem'))
766
767 self._PRW_encounter_type.SetText(suppress_smarts = True)
768 self._PRW_encounter_start.SetText(suppress_smarts = True)
769 self._PRW_encounter_end.SetText(suppress_smarts = True)
770 self._TCTRL_rfe.SetValue(u'')
771 self._TCTRL_aoe.SetValue(u'')
772
773 self._NB_soap_editors.DeleteAllPages()
774 self._NB_soap_editors.add_editor()
775
777 """Update health problems list."""
778
779 self._LCTRL_active_problems.set_string_items()
780
781 emr = self.__pat.get_emr()
782 problems = emr.get_problems (
783 include_closed_episodes = self._CHBOX_show_closed_episodes.IsChecked(),
784 include_irrelevant_issues = self._CHBOX_irrelevant_issues.IsChecked()
785 )
786
787 list_items = []
788 active_problems = []
789 for problem in problems:
790 if not problem['problem_active']:
791 if not problem['is_potential_problem']:
792 continue
793
794 active_problems.append(problem)
795
796 if problem['type'] == 'issue':
797 issue = emr.problem2issue(problem)
798 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue'])
799 if last_encounter is None:
800 last = issue['modified_when'].strftime('%m/%Y')
801 else:
802 last = last_encounter['last_affirmed'].strftime('%m/%Y')
803
804 list_items.append([last, problem['problem'], gmTools.u_down_left_arrow])
805
806 elif problem['type'] == 'episode':
807 epi = emr.problem2episode(problem)
808 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode'])
809 if last_encounter is None:
810 last = epi['episode_modified_when'].strftime('%m/%Y')
811 else:
812 last = last_encounter['last_affirmed'].strftime('%m/%Y')
813
814 list_items.append ([
815 last,
816 problem['problem'],
817 gmTools.coalesce(initial = epi['health_issue'], instead = u'?')
818 ])
819
820 self._LCTRL_active_problems.set_string_items(items = list_items)
821 self._LCTRL_active_problems.set_column_widths()
822 self._LCTRL_active_problems.set_data(data = active_problems)
823
824 showing_potential_problems = (
825 self._CHBOX_show_closed_episodes.IsChecked()
826 or
827 self._CHBOX_irrelevant_issues.IsChecked()
828 )
829 if showing_potential_problems:
830 self._SZR_problem_list_staticbox.SetLabel(_('%s (active+potential) problems') % len(list_items))
831 else:
832 self._SZR_problem_list_staticbox.SetLabel(_('%s active problems') % len(list_items))
833
834 return True
835
837 soap = u''
838 emr = self.__pat.get_emr()
839 prev_enc = emr.get_last_but_one_encounter(issue_id = problem['pk_health_issue'])
840 if prev_enc is not None:
841 soap += prev_enc.format (
842 issues = [ problem['pk_health_issue'] ],
843 with_soap = True,
844 with_docs = False,
845 with_tests = False,
846 patient = self.__pat,
847 fancy_header = False,
848 with_rfe_aoe = True
849 )
850
851 tmp = emr.active_encounter.format_soap (
852 soap_cats = 'soap',
853 emr = emr,
854 issues = [ problem['pk_health_issue'] ],
855 )
856 if len(tmp) > 0:
857 soap += _('Current encounter:') + u'\n'
858 soap += u'\n'.join(tmp) + u'\n'
859
860 if problem['summary'] is not None:
861 soap += u'\n-- %s ----------\n%s' % (
862 _('Cumulative summary'),
863 gmTools.wrap (
864 text = problem['summary'],
865 width = 45,
866 initial_indent = u' ',
867 subsequent_indent = u' '
868 ).strip('\n')
869 )
870
871 return soap
872
874 soap = u''
875 emr = self.__pat.get_emr()
876 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_episode'])
877 if prev_enc is not None:
878 soap += prev_enc.format (
879 episodes = [ problem['pk_episode'] ],
880 with_soap = True,
881 with_docs = False,
882 with_tests = False,
883 patient = self.__pat,
884 fancy_header = False,
885 with_rfe_aoe = True
886 )
887 else:
888 if problem['pk_health_issue'] is not None:
889 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_health_issue'])
890 if prev_enc is not None:
891 soap += prev_enc.format (
892 with_soap = True,
893 with_docs = False,
894 with_tests = False,
895 patient = self.__pat,
896 issues = [ problem['pk_health_issue'] ],
897 fancy_header = False,
898 with_rfe_aoe = True
899 )
900
901 tmp = emr.active_encounter.format_soap (
902 soap_cats = 'soap',
903 emr = emr,
904 issues = [ problem['pk_health_issue'] ],
905 )
906 if len(tmp) > 0:
907 soap += _('Current encounter:') + u'\n'
908 soap += u'\n'.join(tmp) + u'\n'
909
910 if problem['summary'] is not None:
911 soap += u'\n-- %s ----------\n%s' % (
912 _('Cumulative summary'),
913 gmTools.wrap (
914 text = problem['summary'],
915 width = 45,
916 initial_indent = u' ',
917 subsequent_indent = u' '
918 ).strip('\n')
919 )
920
921 return soap
922
925
949
951 """This refreshes the recent-notes part."""
952
953 soap = u''
954 caption = u'<?>'
955
956 if problem['type'] == u'issue':
957 caption = problem['problem'][:35]
958 soap = self.__get_soap_for_issue_problem(problem = problem)
959
960 elif problem['type'] == u'episode':
961 caption = problem['problem'][:35]
962 soap = self.__get_soap_for_episode_problem(problem = problem)
963
964 self._TCTRL_recent_notes.SetValue(soap)
965 self._TCTRL_recent_notes.ShowPosition(self._TCTRL_recent_notes.GetLastPosition())
966 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on %s%s%s') % (
967 gmTools.u_left_double_angle_quote,
968 caption,
969 gmTools.u_right_double_angle_quote
970 ))
971
972 self._TCTRL_recent_notes.Refresh()
973
974 return True
975
977 """Update encounter fields."""
978
979 emr = self.__pat.get_emr()
980 enc = emr.active_encounter
981 self._PRW_encounter_type.SetText(value = enc['l10n_type'], data = enc['pk_type'])
982
983 fts = gmDateTime.cFuzzyTimestamp (
984 timestamp = enc['started'],
985 accuracy = gmDateTime.acc_minutes
986 )
987 self._PRW_encounter_start.SetText(fts.format_accurately(), data = fts)
988
989 fts = gmDateTime.cFuzzyTimestamp (
990 timestamp = enc['last_affirmed'],
991 accuracy = gmDateTime.acc_minutes
992 )
993 self._PRW_encounter_end.SetText(fts.format_accurately(), data = fts)
994
995 self._TCTRL_rfe.SetValue(gmTools.coalesce(enc['reason_for_encounter'], u''))
996 val, data = self._PRW_rfe_codes.generic_linked_codes2item_dict(enc.generic_codes_rfe)
997 self._PRW_rfe_codes.SetText(val, data)
998
999 self._TCTRL_aoe.SetValue(gmTools.coalesce(enc['assessment_of_encounter'], u''))
1000 val, data = self._PRW_aoe_codes.generic_linked_codes2item_dict(enc.generic_codes_aoe)
1001 self._PRW_aoe_codes.SetText(val, data)
1002
1003 self._PRW_encounter_type.Refresh()
1004 self._PRW_encounter_start.Refresh()
1005 self._PRW_encounter_end.Refresh()
1006 self._TCTRL_rfe.Refresh()
1007 self._PRW_rfe_codes.Refresh()
1008 self._TCTRL_aoe.Refresh()
1009 self._PRW_aoe_codes.Refresh()
1010
1012 """Assumes that the field data is valid."""
1013
1014 emr = self.__pat.get_emr()
1015 enc = emr.active_encounter
1016
1017 data = {
1018 'pk_type': self._PRW_encounter_type.GetData(),
1019 'reason_for_encounter': gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u''),
1020 'assessment_of_encounter': gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
1021 'pk_location': enc['pk_location'],
1022 'pk_patient': enc['pk_patient']
1023 }
1024
1025 if self._PRW_encounter_start.GetData() is None:
1026 data['started'] = None
1027 else:
1028 data['started'] = self._PRW_encounter_start.GetData().get_pydt()
1029
1030 if self._PRW_encounter_end.GetData() is None:
1031 data['last_affirmed'] = None
1032 else:
1033 data['last_affirmed'] = self._PRW_encounter_end.GetData().get_pydt()
1034
1035 return not enc.same_payload(another_object = data)
1036
1038
1039 found_error = False
1040
1041 if self._PRW_encounter_type.GetData() is None:
1042 found_error = True
1043 msg = _('Cannot save encounter: missing type.')
1044
1045 if self._PRW_encounter_start.GetData() is None:
1046 found_error = True
1047 msg = _('Cannot save encounter: missing start time.')
1048
1049 if self._PRW_encounter_end.GetData() is None:
1050 found_error = True
1051 msg = _('Cannot save encounter: missing end time.')
1052
1053 if found_error:
1054 gmDispatcher.send(signal = 'statustext', msg = msg, beep = True)
1055 return False
1056
1057 return True
1058
1059
1060
1062 """Configure enabled event signals."""
1063
1064 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1065 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1066 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db)
1067 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
1068 gmDispatcher.connect(signal = u'episode_code_mod_db', receiver = self._on_episode_issue_mod_db)
1069 gmDispatcher.connect(signal = u'doc_mod_db', receiver = self._on_doc_mod_db)
1070 gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified)
1071 gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_switched)
1072 gmDispatcher.connect(signal = u'rfe_code_mod_db', receiver = self._on_current_encounter_modified)
1073 gmDispatcher.connect(signal = u'aoe_code_mod_db', receiver = self._on_current_encounter_modified)
1074
1075
1076 self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback)
1077 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
1078
1080 """Another patient is about to be activated.
1081
1082 Patient change will not proceed before this returns True.
1083 """
1084
1085
1086 if not self.__pat.connected:
1087 return True
1088 return self._NB_soap_editors.warn_on_unsaved_soap()
1089
1091 """The client is about to be shut down.
1092
1093 Shutdown will not proceed before this returns.
1094 """
1095 if not self.__pat.connected:
1096 return True
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112 emr = self.__pat.get_emr()
1113 saved = self._NB_soap_editors.save_all_editors (
1114 emr = emr,
1115 episode_name_candidates = [
1116 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
1117 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'')
1118 ]
1119 )
1120 if not saved:
1121 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
1122 return True
1123
1125 wx.CallAfter(self.__on_pre_patient_selection)
1126
1128 self.__reset_ui_content()
1129
1131 wx.CallAfter(self._schedule_data_reget)
1132 self.__patient_just_changed = True
1133
1135 wx.CallAfter(self.__refresh_current_editor)
1136
1138 wx.CallAfter(self._schedule_data_reget)
1139
1141 wx.CallAfter(self.__refresh_encounter)
1142
1144 wx.CallAfter(self.__on_current_encounter_switched)
1145
1147 self.__refresh_encounter()
1148
1149
1150
1152 """Show related note at the bottom."""
1153 pass
1154
1166
1168 """Show related note at the bottom."""
1169 emr = self.__pat.get_emr()
1170 self.__refresh_recent_notes (
1171 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1172 )
1173
1175 """Open progress note editor for this problem.
1176 """
1177 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1178 if problem is None:
1179 return True
1180
1181 dbcfg = gmCfg.cCfgSQL()
1182 allow_duplicate_editors = bool(dbcfg.get2 (
1183 option = u'horstspace.soap_editor.allow_same_episode_multiple_times',
1184 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1185 bias = u'user',
1186 default = False
1187 ))
1188 if self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors):
1189 return True
1190
1191 gmGuiHelpers.gm_show_error (
1192 aMessage = _(
1193 'Cannot open progress note editor for\n\n'
1194 '[%s].\n\n'
1195 ) % problem['problem'],
1196 aTitle = _('opening progress note editor')
1197 )
1198 event.Skip()
1199 return False
1200
1202 self.__refresh_problem_list()
1203
1205 self.__refresh_problem_list()
1206
1207
1208
1212
1216
1220
1231
1254
1259
1260
1261
1265
1289
1290
1291
1300
1312
1313
1314
1316 self.__refresh_problem_list()
1317 self.__refresh_encounter()
1318 self.__setup_initial_patient_editors()
1319 return True
1320
1504
1505 from Gnumed.wxGladeWidgets import wxgSoapNoteExpandoEditAreaPnl
1506
1508 """An Edit Area like panel for entering progress notes.
1509
1510 Subjective: Codes:
1511 expando text ctrl
1512 Objective: Codes:
1513 expando text ctrl
1514 Assessment: Codes:
1515 expando text ctrl
1516 Plan: Codes:
1517 expando text ctrl
1518 visual progress notes
1519 panel with images
1520 Episode summary: Codes:
1521 text ctrl
1522
1523 - knows the problem this edit area is about
1524 - can deal with issue or episode type problems
1525 """
1526
1528
1529 try:
1530 self.problem = kwargs['problem']
1531 del kwargs['problem']
1532 except KeyError:
1533 self.problem = None
1534
1535 wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl.__init__(self, *args, **kwargs)
1536
1537 self.soap_fields = [
1538 self._TCTRL_Soap,
1539 self._TCTRL_sOap,
1540 self._TCTRL_soAp,
1541 self._TCTRL_soaP
1542 ]
1543
1544 self.__init_ui()
1545 self.__register_interests()
1546
1553
1557
1559 self._TCTRL_episode_summary.SetValue(u'')
1560 self._PRW_episode_codes.SetText(u'', self._PRW_episode_codes.list2data_dict([]))
1561 self._LBL_summary.SetLabel(_('Episode summary'))
1562
1563
1564 if self.problem is None:
1565 return
1566
1567
1568 if self.problem['type'] == u'issue':
1569 return
1570
1571
1572 caption = _(u'Summary (%s)') % (
1573 gmDateTime.pydt_strftime (
1574 self.problem['modified_when'],
1575 format = '%B %Y',
1576 accuracy = gmDateTime.acc_days
1577 )
1578 )
1579 self._LBL_summary.SetLabel(caption)
1580
1581 if self.problem['summary'] is not None:
1582 self._TCTRL_episode_summary.SetValue(self.problem['summary'].strip())
1583
1584 val, data = self._PRW_episode_codes.generic_linked_codes2item_dict(self.problem.generic_codes)
1585 self._PRW_episode_codes.SetText(val, data)
1586
1588 if self.problem is None:
1589 self._PNL_visual_soap.refresh(document_folder = None)
1590 return
1591
1592 if self.problem['type'] == u'issue':
1593 self._PNL_visual_soap.refresh(document_folder = None)
1594 return
1595
1596 if self.problem['type'] == u'episode':
1597 pat = gmPerson.gmCurrentPatient()
1598 doc_folder = pat.get_document_folder()
1599 emr = pat.get_emr()
1600 self._PNL_visual_soap.refresh (
1601 document_folder = doc_folder,
1602 episodes = [self.problem['pk_episode']],
1603 encounter = emr.active_encounter['pk_encounter']
1604 )
1605 return
1606
1608 for field in self.soap_fields:
1609 field.SetValue(u'')
1610 self._TCTRL_episode_summary.SetValue(u'')
1611 self._LBL_summary.SetLabel(_('Episode summary'))
1612 self._PRW_episode_codes.SetText(u'', self._PRW_episode_codes.list2data_dict([]))
1613 self._PNL_visual_soap.clear()
1614
1616 fname, discard_unmodified = select_visual_progress_note_template(parent = self)
1617 if fname is None:
1618 return False
1619
1620 if self.problem is None:
1621 issue = None
1622 episode = None
1623 elif self.problem['type'] == 'issue':
1624 issue = self.problem['pk_health_issue']
1625 episode = None
1626 else:
1627 issue = self.problem['pk_health_issue']
1628 episode = gmEMRStructItems.problem2episode(self.problem)
1629
1630 wx.CallAfter (
1631 edit_visual_progress_note,
1632 filename = fname,
1633 episode = episode,
1634 discard_unmodified = discard_unmodified,
1635 health_issue = issue
1636 )
1637
1638 - def save(self, emr=None, episode_name_candidates=None, encounter=None):
1639
1640 if self.empty:
1641 return True
1642
1643
1644 if (self.problem is None) or (self.problem['type'] == 'issue'):
1645 episode = self.__create_new_episode(emr = emr, episode_name_candidates = episode_name_candidates)
1646
1647 else:
1648 episode = emr.problem2episode(self.problem)
1649
1650 if encounter is None:
1651 encounter = emr.current_encounter['pk_encounter']
1652
1653 soap_notes = []
1654 for note in self.soap:
1655 saved, data = gmClinNarrative.create_clin_narrative (
1656 soap_cat = note[0],
1657 narrative = note[1],
1658 episode_id = episode['pk_episode'],
1659 encounter_id = encounter
1660 )
1661 if saved:
1662 soap_notes.append(data)
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675 if self.problem is not None:
1676 if self.problem['type'] == 'episode':
1677 episode['summary'] = self._TCTRL_episode_summary.GetValue().strip()
1678 episode.save()
1679
1680
1681 episode.generic_codes = [ d['data'] for d in self._PRW_episode_codes.GetData() ]
1682
1683 return True
1684
1685
1686
1688
1689 episode_name_candidates.append(self._TCTRL_episode_summary.GetValue().strip())
1690 for candidate in episode_name_candidates:
1691 if candidate is None:
1692 continue
1693 epi_name = candidate.strip().replace('\r', '//').replace('\n', '//')
1694 break
1695
1696 dlg = wx.TextEntryDialog (
1697 parent = self,
1698 message = _('Enter a short working name for this new problem:'),
1699 caption = _('Creating a problem (episode) to save the notelet under ...'),
1700 defaultValue = epi_name,
1701 style = wx.OK | wx.CANCEL | wx.CENTRE
1702 )
1703 decision = dlg.ShowModal()
1704 if decision != wx.ID_OK:
1705 return None
1706
1707 epi_name = dlg.GetValue().strip()
1708 if epi_name == u'':
1709 gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note'))
1710 return None
1711
1712
1713 new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True)
1714 new_episode['summary'] = self._TCTRL_episode_summary.GetValue().strip()
1715 new_episode.save()
1716
1717 if self.problem is not None:
1718 issue = emr.problem2issue(self.problem)
1719 if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True):
1720 gmGuiHelpers.gm_show_warning (
1721 _(
1722 'The new episode:\n'
1723 '\n'
1724 ' "%s"\n'
1725 '\n'
1726 'will remain unassociated despite the editor\n'
1727 'having been invoked from the health issue:\n'
1728 '\n'
1729 ' "%s"'
1730 ) % (
1731 new_episode['description'],
1732 issue['description']
1733 ),
1734 _('saving progress note')
1735 )
1736
1737 return new_episode
1738
1739
1740
1742 for field in self.soap_fields:
1743 wx_expando.EVT_ETC_LAYOUT_NEEDED(field, field.GetId(), self._on_expando_needs_layout)
1744 wx_expando.EVT_ETC_LAYOUT_NEEDED(self._TCTRL_episode_summary, self._TCTRL_episode_summary.GetId(), self._on_expando_needs_layout)
1745
1747
1748
1749
1750
1751 self.FitInside()
1752
1753 if self.HasScrollbar(wx.VERTICAL):
1754
1755 expando = self.FindWindowById(evt.GetId())
1756 y_expando = expando.GetPositionTuple()[1]
1757 h_expando = expando.GetSizeTuple()[1]
1758 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1
1759 y_cursor = int(round((float(line_cursor) / expando.NumberOfLines) * h_expando))
1760 y_desired_visible = y_expando + y_cursor
1761
1762 y_view = self.ViewStart[1]
1763 h_view = self.GetClientSizeTuple()[1]
1764
1765
1766
1767
1768
1769
1770
1771
1772 if y_desired_visible < y_view:
1773
1774 self.Scroll(0, y_desired_visible)
1775
1776 if y_desired_visible > h_view:
1777
1778 self.Scroll(0, y_desired_visible)
1779
1780
1781
1783 soap_notes = []
1784
1785 tmp = self._TCTRL_Soap.GetValue().strip()
1786 if tmp != u'':
1787 soap_notes.append(['s', tmp])
1788
1789 tmp = self._TCTRL_sOap.GetValue().strip()
1790 if tmp != u'':
1791 soap_notes.append(['o', tmp])
1792
1793 tmp = self._TCTRL_soAp.GetValue().strip()
1794 if tmp != u'':
1795 soap_notes.append(['a', tmp])
1796
1797 tmp = self._TCTRL_soaP.GetValue().strip()
1798 if tmp != u'':
1799 soap_notes.append(['p', tmp])
1800
1801 return soap_notes
1802
1803 soap = property(_get_soap, lambda x:x)
1804
1806
1807
1808 for field in self.soap_fields:
1809 if field.GetValue().strip() != u'':
1810 return False
1811
1812
1813 summary = self._TCTRL_episode_summary.GetValue().strip()
1814 if self.problem is None:
1815 if summary != u'':
1816 return False
1817 elif self.problem['type'] == u'issue':
1818 if summary != u'':
1819 return False
1820 else:
1821 if self.problem['summary'] is None:
1822 if summary != u'':
1823 return False
1824 else:
1825 if summary != self.problem['summary'].strip():
1826 return False
1827
1828
1829 new_codes = self._PRW_episode_codes.GetData()
1830 if self.problem is None:
1831 if len(new_codes) > 0:
1832 return False
1833 elif self.problem['type'] == u'issue':
1834 if len(new_codes) > 0:
1835 return False
1836 else:
1837 old_code_pks = self.problem.generic_codes
1838 if len(old_code_pks) != len(new_codes):
1839 return False
1840 for code in new_codes:
1841 if code['data'] not in old_code_pks:
1842 return False
1843
1844 return True
1845
1846 empty = property(_get_empty, lambda x:x)
1847
1848 -class cSoapLineTextCtrl(wx_expando.ExpandoTextCtrl):
1849
1850 - def __init__(self, *args, **kwargs):
1851
1852 wx_expando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
1853
1854 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+")
1855
1856 self.__register_interests()
1857
1858
1859
1860 - def _wrapLine(self, line, dc, width):
1861
1862 if (wx.MAJOR_VERSION >= 2) and (wx.MINOR_VERSION > 8):
1863 return super(cSoapLineTextCtrl, self)._wrapLine(line, dc, width)
1864
1865
1866
1867
1868 pte = dc.GetPartialTextExtents(line)
1869 width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X)
1870 idx = 0
1871 start = 0
1872 count = 0
1873 spc = -1
1874 while idx < len(pte):
1875 if line[idx] == ' ':
1876 spc = idx
1877 if pte[idx] - start > width:
1878
1879 count += 1
1880
1881 if spc != -1:
1882 idx = spc + 1
1883 spc = -1
1884 if idx < len(pte):
1885 start = pte[idx]
1886 else:
1887 idx += 1
1888 return count
1889
1890
1891
1893
1894
1895 wx.EVT_CHAR(self, self.__on_char)
1896 wx.EVT_SET_FOCUS(self, self.__on_focus)
1897
1898 - def __on_focus(self, evt):
1899 evt.Skip()
1900 wx.CallAfter(self._after_on_focus)
1901
1902 - def _after_on_focus(self):
1903
1904 evt = wx.PyCommandEvent(wx_expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
1905 evt.SetEventObject(self)
1906
1907
1908
1909
1910 self.GetEventHandler().ProcessEvent(evt)
1911
1912 - def __on_char(self, evt):
1913 char = unichr(evt.GetUnicodeKey())
1914
1915 if self.LastPosition == 1:
1916 evt.Skip()
1917 return
1918
1919 explicit_expansion = False
1920 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT):
1921 if evt.GetKeyCode() != 13:
1922 evt.Skip()
1923 return
1924 explicit_expansion = True
1925
1926 if not explicit_expansion:
1927 if self.__keyword_separators.match(char) is None:
1928 evt.Skip()
1929 return
1930
1931 caret_pos, line_no = self.PositionToXY(self.InsertionPoint)
1932 line = self.GetLineText(line_no)
1933 word = self.__keyword_separators.split(line[:caret_pos])[-1]
1934
1935 if (
1936 (not explicit_expansion)
1937 and
1938 (word != u'$$steffi')
1939 and
1940 (word not in [ r[0] for r in gmPG2.get_text_expansion_keywords() ])
1941 ):
1942 evt.Skip()
1943 return
1944
1945 start = self.InsertionPoint - len(word)
1946 wx.CallAfter(self.replace_keyword_with_expansion, word, start, explicit_expansion)
1947
1948 evt.Skip()
1949 return
1950
1951 - def replace_keyword_with_expansion(self, keyword=None, position=None, show_list=False):
1952
1953 if show_list:
1954 candidates = gmPG2.get_keyword_expansion_candidates(keyword = keyword)
1955 if len(candidates) == 0:
1956 return
1957 if len(candidates) == 1:
1958 keyword = candidates[0]
1959 else:
1960 keyword = gmListWidgets.get_choices_from_list (
1961 parent = self,
1962 msg = _(
1963 'Several macros match the keyword [%s].\n'
1964 '\n'
1965 'Please select the expansion you want to happen.'
1966 ) % keyword,
1967 caption = _('Selecting text macro'),
1968 choices = candidates,
1969 columns = [_('Keyword')],
1970 single_selection = True,
1971 can_return_empty = False
1972 )
1973 if keyword is None:
1974 return
1975
1976 expansion = gmPG2.expand_keyword(keyword = keyword)
1977
1978 if expansion is None:
1979 return
1980
1981 if expansion == u'':
1982 return
1983
1984 self.Replace (
1985 position,
1986 position + len(keyword),
1987 expansion
1988 )
1989
1990 self.SetInsertionPoint(position + len(expansion) + 1)
1991 self.ShowPosition(position + len(expansion) + 1)
1992
1993 return
1994
1995
1996
2027
2028 cmd = gmCfgWidgets.configure_string_option (
2029 message = _(
2030 'Enter the shell command with which to start\n'
2031 'the image editor for visual progress notes.\n'
2032 '\n'
2033 'Any "%(img)s" included with the arguments\n'
2034 'will be replaced by the file name of the\n'
2035 'note template.'
2036 ),
2037 option = u'external.tools.visual_soap_editor_cmd',
2038 bias = 'user',
2039 default_value = None,
2040 validator = is_valid
2041 )
2042
2043 return cmd
2044
2046 if parent is None:
2047 parent = wx.GetApp().GetTopWindow()
2048
2049 dlg = wx.FileDialog (
2050 parent = parent,
2051 message = _('Choose file to use as template for new visual progress note'),
2052 defaultDir = os.path.expanduser('~'),
2053 defaultFile = '',
2054
2055 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST
2056 )
2057 result = dlg.ShowModal()
2058
2059 if result == wx.ID_CANCEL:
2060 dlg.Destroy()
2061 return None
2062
2063 full_filename = dlg.GetPath()
2064 dlg.Hide()
2065 dlg.Destroy()
2066 return full_filename
2067
2069
2070 if parent is None:
2071 parent = wx.GetApp().GetTopWindow()
2072
2073 dlg = gmGuiHelpers.c3ButtonQuestionDlg (
2074 parent,
2075 -1,
2076 caption = _('Visual progress note source'),
2077 question = _('From which source do you want to pick the image template ?'),
2078 button_defs = [
2079 {'label': _('Database'), 'tooltip': _('List of templates in the database.'), 'default': True},
2080 {'label': _('File'), 'tooltip': _('Files in the filesystem.'), 'default': False},
2081 {'label': _('Device'), 'tooltip': _('Image capture devices (scanners, cameras, etc)'), 'default': False}
2082 ]
2083 )
2084 result = dlg.ShowModal()
2085 dlg.Destroy()
2086
2087
2088 if result == wx.ID_YES:
2089 _log.debug('visual progress note template from: database template')
2090 from Gnumed.wxpython import gmFormWidgets
2091 template = gmFormWidgets.manage_form_templates (
2092 parent = parent,
2093 template_types = [gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE],
2094 active_only = True
2095 )
2096 if template is None:
2097 return (None, None)
2098 filename = template.export_to_file()
2099 if filename is None:
2100 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note template for [%s].') % template['name_long'])
2101 return (None, None)
2102 return (filename, True)
2103
2104
2105 if result == wx.ID_NO:
2106 _log.debug('visual progress note template from: disk file')
2107 fname = select_file_as_visual_progress_note_template(parent = parent)
2108 if fname is None:
2109 return (None, None)
2110
2111 ext = os.path.splitext(fname)[1]
2112 tmp_name = gmTools.get_unique_filename(suffix = ext)
2113 _log.debug('visual progress note from file: [%s] -> [%s]', fname, tmp_name)
2114 shutil.copy2(fname, tmp_name)
2115 return (tmp_name, False)
2116
2117
2118 if result == wx.ID_CANCEL:
2119 _log.debug('visual progress note template from: image capture device')
2120 fnames = gmDocumentWidgets.acquire_images_from_capture_device(device = None, calling_window = parent)
2121 if fnames is None:
2122 return (None, None)
2123 if len(fnames) == 0:
2124 return (None, None)
2125 return (fnames[0], False)
2126
2127 _log.debug('no visual progress note template source selected')
2128 return (None, None)
2129
2131 """This assumes <filename> contains an image which can be handled by the configured image editor."""
2132
2133 if doc_part is not None:
2134 filename = doc_part.export_to_file()
2135 if filename is None:
2136 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.'))
2137 return None
2138
2139 dbcfg = gmCfg.cCfgSQL()
2140 cmd = dbcfg.get2 (
2141 option = u'external.tools.visual_soap_editor_cmd',
2142 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2143 bias = 'user'
2144 )
2145
2146 if cmd is None:
2147 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = False)
2148 cmd = configure_visual_progress_note_editor()
2149 if cmd is None:
2150 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = True)
2151 return None
2152
2153 if u'%(img)s' in cmd:
2154 cmd % {u'img': filename}
2155 else:
2156 cmd = u'%s %s' % (cmd, filename)
2157
2158 if discard_unmodified:
2159 original_stat = os.stat(filename)
2160 original_md5 = gmTools.file2md5(filename)
2161
2162 success = gmShellAPI.run_command_in_shell(cmd, blocking = True)
2163 if not success:
2164 gmGuiHelpers.gm_show_error (
2165 _(
2166 'There was a problem with running the editor\n'
2167 'for visual progress notes.\n'
2168 '\n'
2169 ' [%s]\n'
2170 '\n'
2171 ) % cmd,
2172 _('Editing visual progress note')
2173 )
2174 return None
2175
2176 try:
2177 open(filename, 'r').close()
2178 except StandardError:
2179 _log.exception('problem accessing visual progress note file [%s]', filename)
2180 gmGuiHelpers.gm_show_error (
2181 _(
2182 'There was a problem reading the visual\n'
2183 'progress note from the file:\n'
2184 '\n'
2185 ' [%s]\n'
2186 '\n'
2187 ) % filename,
2188 _('Saving visual progress note')
2189 )
2190 return None
2191
2192 if discard_unmodified:
2193 modified_stat = os.stat(filename)
2194
2195 if original_stat.st_size == modified_stat.st_size:
2196 modified_md5 = gmTools.file2md5(filename)
2197
2198 if original_md5 == modified_md5:
2199 _log.debug('visual progress note (template) not modified')
2200
2201 msg = _(
2202 u'You either created a visual progress note from a template\n'
2203 u'in the database (rather than from a file on disk) or you\n'
2204 u'edited an existing visual progress note.\n'
2205 u'\n'
2206 u'The template/original was not modified at all, however.\n'
2207 u'\n'
2208 u'Do you still want to save the unmodified image as a\n'
2209 u'visual progress note into the EMR of the patient ?\n'
2210 )
2211 save_unmodified = gmGuiHelpers.gm_show_question (
2212 msg,
2213 _('Saving visual progress note')
2214 )
2215 if not save_unmodified:
2216 _log.debug('user discarded unmodified note')
2217 return
2218
2219 if doc_part is not None:
2220 doc_part.update_data_from_file(fname = filename)
2221 doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2222 return None
2223
2224 if not isinstance(episode, gmEMRStructItems.cEpisode):
2225 if episode is None:
2226 episode = _('visual progress notes')
2227 pat = gmPerson.gmCurrentPatient()
2228 emr = pat.get_emr()
2229 episode = emr.add_episode(episode_name = episode.strip(), pk_health_issue = health_issue, is_open = False)
2230
2231 doc = gmDocumentWidgets.save_file_as_new_document (
2232 filename = filename,
2233 document_type = gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE,
2234 episode = episode,
2235 unlock_patient = True
2236 )
2237 doc.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2238
2239 return doc
2240
2242 """Phrasewheel to allow selection of visual SOAP template."""
2243
2245
2246 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
2247
2248 query = u"""
2249 SELECT
2250 pk AS data,
2251 name_short AS list_label,
2252 name_sort AS field_label
2253 FROM
2254 ref.paperwork_templates
2255 WHERE
2256 fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND (
2257 name_long %%(fragment_condition)s
2258 OR
2259 name_short %%(fragment_condition)s
2260 )
2261 ORDER BY list_label
2262 LIMIT 15
2263 """ % gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE
2264
2265 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
2266 mp.setThresholds(2, 3, 5)
2267
2268 self.matcher = mp
2269 self.selection_only = True
2270
2276
2277 from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl
2278
2280
2285
2286
2287
2288 - def refresh(self, document_folder=None, episodes=None, encounter=None):
2289
2290 self.clear()
2291 if document_folder is not None:
2292 soap_docs = document_folder.get_visual_progress_notes(episodes = episodes, encounter = encounter)
2293 if len(soap_docs) > 0:
2294 for soap_doc in soap_docs:
2295 parts = soap_doc.parts
2296 if len(parts) == 0:
2297 continue
2298 part = parts[0]
2299 fname = part.export_to_file()
2300 if fname is None:
2301 continue
2302
2303
2304 img = gmGuiHelpers.file2scaled_image (
2305 filename = fname,
2306 height = 30
2307 )
2308
2309 bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER)
2310
2311
2312 img = gmGuiHelpers.file2scaled_image (
2313 filename = fname,
2314 height = 150
2315 )
2316 tip = agw_stt.SuperToolTip (
2317 u'',
2318 bodyImage = img,
2319 header = _('Created: %s') % part['date_generated'].strftime('%Y %B %d').decode(gmI18N.get_encoding()),
2320 footer = gmTools.coalesce(part['doc_comment'], u'').strip()
2321 )
2322 tip.SetTopGradientColor('white')
2323 tip.SetMiddleGradientColor('white')
2324 tip.SetBottomGradientColor('white')
2325 tip.SetTarget(bmp)
2326
2327 bmp.doc_part = part
2328 bmp.Bind(wx.EVT_LEFT_UP, self._on_bitmap_leftclicked)
2329
2330 self._SZR_soap.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM | wx.EXPAND, 3)
2331 self.__bitmaps.append(bmp)
2332
2333 self.GetParent().Layout()
2334
2336 for child_idx in range(len(self._SZR_soap.GetChildren())):
2337 self._SZR_soap.Detach(child_idx)
2338 for bmp in self.__bitmaps:
2339 bmp.Destroy()
2340 self.__bitmaps = []
2341
2343 wx.CallAfter (
2344 edit_visual_progress_note,
2345 doc_part = evt.GetEventObject().doc_part,
2346 discard_unmodified = True
2347 )
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589 if __name__ == '__main__':
2590
2591 if len(sys.argv) < 2:
2592 sys.exit()
2593
2594 if sys.argv[1] != 'test':
2595 sys.exit()
2596
2597 gmI18N.activate_locale()
2598 gmI18N.install_domain(domain = 'gnumed')
2599
2600
2609
2616
2629
2630
2631 test_cSoapNoteExpandoEditAreaPnl()
2632
2633
2634
2635