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
759 self._PRW_encounter_start.add_callback_on_lose_focus(callback = self._on_encounter_start_lost_focus)
760
762 start = self._PRW_encounter_start.GetData().get_pydt()
763 if start is None:
764 return
765
766 end = self._PRW_encounter_end.GetData().get_pydt()
767 if end is None:
768 fts = gmDateTime.cFuzzyTimestamp (
769 timestamp = start,
770 accuracy = gmDateTime.acc_minutes
771 )
772 self._PRW_encounter_end.SetText(fts.format_accurately(), data = fts)
773 return
774
775 if start > end:
776 end = end.replace (
777 year = start.year,
778 month = start.month,
779 day = start.day
780 )
781 fts = gmDateTime.cFuzzyTimestamp (
782 timestamp = end,
783 accuracy = gmDateTime.acc_minutes
784 )
785 self._PRW_encounter_end.SetText(fts.format_accurately(), data = fts)
786 return
787
788 emr = self.__pat.get_emr()
789 if start != emr.active_encounter['started']:
790 end = end.replace (
791 year = start.year,
792 month = start.month,
793 day = start.day
794 )
795 fts = gmDateTime.cFuzzyTimestamp (
796 timestamp = end,
797 accuracy = gmDateTime.acc_minutes
798 )
799 self._PRW_encounter_end.SetText(fts.format_accurately(), data = fts)
800 return
801
802 return
803
805 """Clear all information from input panel."""
806
807 self._LCTRL_active_problems.set_string_items()
808
809 self._TCTRL_recent_notes.SetValue(u'')
810 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on selected problem'))
811
812 self._PRW_encounter_type.SetText(suppress_smarts = True)
813 self._PRW_encounter_start.SetText(suppress_smarts = True)
814 self._PRW_encounter_end.SetText(suppress_smarts = True)
815 self._TCTRL_rfe.SetValue(u'')
816 self._TCTRL_aoe.SetValue(u'')
817
818 self._NB_soap_editors.DeleteAllPages()
819 self._NB_soap_editors.add_editor()
820
822 """Update health problems list."""
823
824 self._LCTRL_active_problems.set_string_items()
825
826 emr = self.__pat.get_emr()
827 problems = emr.get_problems (
828 include_closed_episodes = self._CHBOX_show_closed_episodes.IsChecked(),
829 include_irrelevant_issues = self._CHBOX_irrelevant_issues.IsChecked()
830 )
831
832 list_items = []
833 active_problems = []
834 for problem in problems:
835 if not problem['problem_active']:
836 if not problem['is_potential_problem']:
837 continue
838
839 active_problems.append(problem)
840
841 if problem['type'] == 'issue':
842 issue = emr.problem2issue(problem)
843 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue'])
844 if last_encounter is None:
845 last = issue['modified_when'].strftime('%m/%Y')
846 else:
847 last = last_encounter['last_affirmed'].strftime('%m/%Y')
848
849 list_items.append([last, problem['problem'], gmTools.u_down_left_arrow])
850
851 elif problem['type'] == 'episode':
852 epi = emr.problem2episode(problem)
853 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode'])
854 if last_encounter is None:
855 last = epi['episode_modified_when'].strftime('%m/%Y')
856 else:
857 last = last_encounter['last_affirmed'].strftime('%m/%Y')
858
859 list_items.append ([
860 last,
861 problem['problem'],
862 gmTools.coalesce(initial = epi['health_issue'], instead = u'?')
863 ])
864
865 self._LCTRL_active_problems.set_string_items(items = list_items)
866 self._LCTRL_active_problems.set_column_widths()
867 self._LCTRL_active_problems.set_data(data = active_problems)
868
869 showing_potential_problems = (
870 self._CHBOX_show_closed_episodes.IsChecked()
871 or
872 self._CHBOX_irrelevant_issues.IsChecked()
873 )
874 if showing_potential_problems:
875 self._SZR_problem_list_staticbox.SetLabel(_('%s (active+potential) problems') % len(list_items))
876 else:
877 self._SZR_problem_list_staticbox.SetLabel(_('%s active problems') % len(list_items))
878
879 return True
880
882 soap = u''
883 emr = self.__pat.get_emr()
884 prev_enc = emr.get_last_but_one_encounter(issue_id = problem['pk_health_issue'])
885 if prev_enc is not None:
886 soap += prev_enc.format (
887 issues = [ problem['pk_health_issue'] ],
888 with_soap = True,
889 with_docs = False,
890 with_tests = False,
891 patient = self.__pat,
892 fancy_header = False,
893 with_rfe_aoe = True
894 )
895
896 tmp = emr.active_encounter.format_soap (
897 soap_cats = 'soap',
898 emr = emr,
899 issues = [ problem['pk_health_issue'] ],
900 )
901 if len(tmp) > 0:
902 soap += _('Current encounter:') + u'\n'
903 soap += u'\n'.join(tmp) + u'\n'
904
905 if problem['summary'] is not None:
906 soap += u'\n-- %s ----------\n%s' % (
907 _('Cumulative summary'),
908 gmTools.wrap (
909 text = problem['summary'],
910 width = 45,
911 initial_indent = u' ',
912 subsequent_indent = u' '
913 ).strip('\n')
914 )
915
916 return soap
917
919 soap = u''
920 emr = self.__pat.get_emr()
921 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_episode'])
922 if prev_enc is not None:
923 soap += prev_enc.format (
924 episodes = [ problem['pk_episode'] ],
925 with_soap = True,
926 with_docs = False,
927 with_tests = False,
928 patient = self.__pat,
929 fancy_header = False,
930 with_rfe_aoe = True
931 )
932 else:
933 if problem['pk_health_issue'] is not None:
934 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_health_issue'])
935 if prev_enc is not None:
936 soap += prev_enc.format (
937 with_soap = True,
938 with_docs = False,
939 with_tests = False,
940 patient = self.__pat,
941 issues = [ problem['pk_health_issue'] ],
942 fancy_header = False,
943 with_rfe_aoe = True
944 )
945
946 tmp = emr.active_encounter.format_soap (
947 soap_cats = 'soap',
948 emr = emr,
949 issues = [ problem['pk_health_issue'] ],
950 )
951 if len(tmp) > 0:
952 soap += _('Current encounter:') + u'\n'
953 soap += u'\n'.join(tmp) + u'\n'
954
955 if problem['summary'] is not None:
956 soap += u'\n-- %s ----------\n%s' % (
957 _('Cumulative summary'),
958 gmTools.wrap (
959 text = problem['summary'],
960 width = 45,
961 initial_indent = u' ',
962 subsequent_indent = u' '
963 ).strip('\n')
964 )
965
966 return soap
967
970
994
996 """This refreshes the recent-notes part."""
997
998 soap = u''
999 caption = u'<?>'
1000
1001 if problem['type'] == u'issue':
1002 caption = problem['problem'][:35]
1003 soap = self.__get_soap_for_issue_problem(problem = problem)
1004
1005 elif problem['type'] == u'episode':
1006 caption = problem['problem'][:35]
1007 soap = self.__get_soap_for_episode_problem(problem = problem)
1008
1009 self._TCTRL_recent_notes.SetValue(soap)
1010 self._TCTRL_recent_notes.ShowPosition(self._TCTRL_recent_notes.GetLastPosition())
1011 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on %s%s%s') % (
1012 gmTools.u_left_double_angle_quote,
1013 caption,
1014 gmTools.u_right_double_angle_quote
1015 ))
1016
1017 self._TCTRL_recent_notes.Refresh()
1018
1019 return True
1020
1022 """Update encounter fields."""
1023
1024 emr = self.__pat.get_emr()
1025 enc = emr.active_encounter
1026 self._PRW_encounter_type.SetText(value = enc['l10n_type'], data = enc['pk_type'])
1027
1028 fts = gmDateTime.cFuzzyTimestamp (
1029 timestamp = enc['started'],
1030 accuracy = gmDateTime.acc_minutes
1031 )
1032 self._PRW_encounter_start.SetText(fts.format_accurately(), data = fts)
1033
1034 fts = gmDateTime.cFuzzyTimestamp (
1035 timestamp = enc['last_affirmed'],
1036 accuracy = gmDateTime.acc_minutes
1037 )
1038 self._PRW_encounter_end.SetText(fts.format_accurately(), data = fts)
1039
1040 self._TCTRL_rfe.SetValue(gmTools.coalesce(enc['reason_for_encounter'], u''))
1041 val, data = self._PRW_rfe_codes.generic_linked_codes2item_dict(enc.generic_codes_rfe)
1042 self._PRW_rfe_codes.SetText(val, data)
1043
1044 self._TCTRL_aoe.SetValue(gmTools.coalesce(enc['assessment_of_encounter'], u''))
1045 val, data = self._PRW_aoe_codes.generic_linked_codes2item_dict(enc.generic_codes_aoe)
1046 self._PRW_aoe_codes.SetText(val, data)
1047
1048 self._PRW_encounter_type.Refresh()
1049 self._PRW_encounter_start.Refresh()
1050 self._PRW_encounter_end.Refresh()
1051 self._TCTRL_rfe.Refresh()
1052 self._PRW_rfe_codes.Refresh()
1053 self._TCTRL_aoe.Refresh()
1054 self._PRW_aoe_codes.Refresh()
1055
1057 """Assumes that the field data is valid."""
1058
1059 emr = self.__pat.get_emr()
1060 enc = emr.active_encounter
1061
1062 data = {
1063 'pk_type': self._PRW_encounter_type.GetData(),
1064 'reason_for_encounter': gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u''),
1065 'assessment_of_encounter': gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
1066 'pk_location': enc['pk_location'],
1067 'pk_patient': enc['pk_patient']
1068 }
1069
1070 if self._PRW_encounter_start.GetData() is None:
1071 data['started'] = None
1072 else:
1073 data['started'] = self._PRW_encounter_start.GetData().get_pydt()
1074
1075 if self._PRW_encounter_end.GetData() is None:
1076 data['last_affirmed'] = None
1077 else:
1078 data['last_affirmed'] = self._PRW_encounter_end.GetData().get_pydt()
1079
1080 return not enc.same_payload(another_object = data)
1081
1083
1084 found_error = False
1085
1086 if self._PRW_encounter_type.GetData() is None:
1087 found_error = True
1088 msg = _('Cannot save encounter: missing type.')
1089
1090 if self._PRW_encounter_start.GetData() is None:
1091 found_error = True
1092 msg = _('Cannot save encounter: missing start time.')
1093
1094 if self._PRW_encounter_end.GetData() is None:
1095 found_error = True
1096 msg = _('Cannot save encounter: missing end time.')
1097
1098 if found_error:
1099 gmDispatcher.send(signal = 'statustext', msg = msg, beep = True)
1100 return False
1101
1102 return True
1103
1104
1105
1107 """Configure enabled event signals."""
1108
1109 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
1110 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
1111 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db)
1112 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
1113 gmDispatcher.connect(signal = u'episode_code_mod_db', receiver = self._on_episode_issue_mod_db)
1114 gmDispatcher.connect(signal = u'doc_mod_db', receiver = self._on_doc_mod_db)
1115 gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified)
1116 gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_switched)
1117 gmDispatcher.connect(signal = u'rfe_code_mod_db', receiver = self._on_current_encounter_modified)
1118 gmDispatcher.connect(signal = u'aoe_code_mod_db', receiver = self._on_current_encounter_modified)
1119
1120
1121 self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback)
1122 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
1123
1125 """Another patient is about to be activated.
1126
1127 Patient change will not proceed before this returns True.
1128 """
1129
1130
1131 if not self.__pat.connected:
1132 return True
1133 return self._NB_soap_editors.warn_on_unsaved_soap()
1134
1136 """The client is about to be shut down.
1137
1138 Shutdown will not proceed before this returns.
1139 """
1140 if not self.__pat.connected:
1141 return True
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157 emr = self.__pat.get_emr()
1158 saved = self._NB_soap_editors.save_all_editors (
1159 emr = emr,
1160 episode_name_candidates = [
1161 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
1162 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'')
1163 ]
1164 )
1165 if not saved:
1166 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
1167 return True
1168
1170 wx.CallAfter(self.__on_pre_patient_selection)
1171
1173 self.__reset_ui_content()
1174
1176 wx.CallAfter(self._schedule_data_reget)
1177 self.__patient_just_changed = True
1178
1180 wx.CallAfter(self.__refresh_current_editor)
1181
1183 wx.CallAfter(self._schedule_data_reget)
1184
1186 wx.CallAfter(self.__refresh_encounter)
1187
1189 wx.CallAfter(self.__on_current_encounter_switched)
1190
1192 self.__refresh_encounter()
1193
1194
1195
1197 """Show related note at the bottom."""
1198 pass
1199
1211
1213 """Show related note at the bottom."""
1214 emr = self.__pat.get_emr()
1215 self.__refresh_recent_notes (
1216 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1217 )
1218
1220 """Open progress note editor for this problem.
1221 """
1222 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
1223 if problem is None:
1224 return True
1225
1226 dbcfg = gmCfg.cCfgSQL()
1227 allow_duplicate_editors = bool(dbcfg.get2 (
1228 option = u'horstspace.soap_editor.allow_same_episode_multiple_times',
1229 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1230 bias = u'user',
1231 default = False
1232 ))
1233 if self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors):
1234 return True
1235
1236 gmGuiHelpers.gm_show_error (
1237 aMessage = _(
1238 'Cannot open progress note editor for\n\n'
1239 '[%s].\n\n'
1240 ) % problem['problem'],
1241 aTitle = _('opening progress note editor')
1242 )
1243 event.Skip()
1244 return False
1245
1247 self.__refresh_problem_list()
1248
1250 self.__refresh_problem_list()
1251
1252
1253
1257
1261
1265
1276
1299
1304
1305
1306
1310
1334
1335
1336
1345
1357
1358
1359
1361 self.__refresh_problem_list()
1362 self.__refresh_encounter()
1363 self.__setup_initial_patient_editors()
1364 return True
1365
1549
1550 from Gnumed.wxGladeWidgets import wxgSoapNoteExpandoEditAreaPnl
1551
1553 """An Edit Area like panel for entering progress notes.
1554
1555 Subjective: Codes:
1556 expando text ctrl
1557 Objective: Codes:
1558 expando text ctrl
1559 Assessment: Codes:
1560 expando text ctrl
1561 Plan: Codes:
1562 expando text ctrl
1563 visual progress notes
1564 panel with images
1565 Episode summary: Codes:
1566 text ctrl
1567
1568 - knows the problem this edit area is about
1569 - can deal with issue or episode type problems
1570 """
1571
1573
1574 try:
1575 self.problem = kwargs['problem']
1576 del kwargs['problem']
1577 except KeyError:
1578 self.problem = None
1579
1580 wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl.__init__(self, *args, **kwargs)
1581
1582 self.soap_fields = [
1583 self._TCTRL_Soap,
1584 self._TCTRL_sOap,
1585 self._TCTRL_soAp,
1586 self._TCTRL_soaP
1587 ]
1588
1589 self.__init_ui()
1590 self.__register_interests()
1591
1598
1602
1604 self._TCTRL_episode_summary.SetValue(u'')
1605 self._PRW_episode_codes.SetText(u'', self._PRW_episode_codes.list2data_dict([]))
1606 self._LBL_summary.SetLabel(_('Episode summary'))
1607
1608
1609 if self.problem is None:
1610 return
1611
1612
1613 if self.problem['type'] == u'issue':
1614 return
1615
1616
1617 caption = _(u'Summary (%s)') % (
1618 gmDateTime.pydt_strftime (
1619 self.problem['modified_when'],
1620 format = '%B %Y',
1621 accuracy = gmDateTime.acc_days
1622 )
1623 )
1624 self._LBL_summary.SetLabel(caption)
1625
1626 if self.problem['summary'] is not None:
1627 self._TCTRL_episode_summary.SetValue(self.problem['summary'].strip())
1628
1629 val, data = self._PRW_episode_codes.generic_linked_codes2item_dict(self.problem.generic_codes)
1630 self._PRW_episode_codes.SetText(val, data)
1631
1633 if self.problem is None:
1634 self._PNL_visual_soap.refresh(document_folder = None)
1635 return
1636
1637 if self.problem['type'] == u'issue':
1638 self._PNL_visual_soap.refresh(document_folder = None)
1639 return
1640
1641 if self.problem['type'] == u'episode':
1642 pat = gmPerson.gmCurrentPatient()
1643 doc_folder = pat.get_document_folder()
1644 emr = pat.get_emr()
1645 self._PNL_visual_soap.refresh (
1646 document_folder = doc_folder,
1647 episodes = [self.problem['pk_episode']],
1648 encounter = emr.active_encounter['pk_encounter']
1649 )
1650 return
1651
1653 for field in self.soap_fields:
1654 field.SetValue(u'')
1655 self._TCTRL_episode_summary.SetValue(u'')
1656 self._LBL_summary.SetLabel(_('Episode summary'))
1657 self._PRW_episode_codes.SetText(u'', self._PRW_episode_codes.list2data_dict([]))
1658 self._PNL_visual_soap.clear()
1659
1661 fname, discard_unmodified = select_visual_progress_note_template(parent = self)
1662 if fname is None:
1663 return False
1664
1665 if self.problem is None:
1666 issue = None
1667 episode = None
1668 elif self.problem['type'] == 'issue':
1669 issue = self.problem['pk_health_issue']
1670 episode = None
1671 else:
1672 issue = self.problem['pk_health_issue']
1673 episode = gmEMRStructItems.problem2episode(self.problem)
1674
1675 wx.CallAfter (
1676 edit_visual_progress_note,
1677 filename = fname,
1678 episode = episode,
1679 discard_unmodified = discard_unmodified,
1680 health_issue = issue
1681 )
1682
1683 - def save(self, emr=None, episode_name_candidates=None, encounter=None):
1684
1685 if self.empty:
1686 return True
1687
1688
1689 if (self.problem is None) or (self.problem['type'] == 'issue'):
1690 episode = self.__create_new_episode(emr = emr, episode_name_candidates = episode_name_candidates)
1691
1692 else:
1693 episode = emr.problem2episode(self.problem)
1694
1695 if encounter is None:
1696 encounter = emr.current_encounter['pk_encounter']
1697
1698 soap_notes = []
1699 for note in self.soap:
1700 saved, data = gmClinNarrative.create_clin_narrative (
1701 soap_cat = note[0],
1702 narrative = note[1],
1703 episode_id = episode['pk_episode'],
1704 encounter_id = encounter
1705 )
1706 if saved:
1707 soap_notes.append(data)
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720 if self.problem is not None:
1721 if self.problem['type'] == 'episode':
1722 episode['summary'] = self._TCTRL_episode_summary.GetValue().strip()
1723 episode.save()
1724
1725
1726 episode.generic_codes = [ d['data'] for d in self._PRW_episode_codes.GetData() ]
1727
1728 return True
1729
1730
1731
1733
1734 episode_name_candidates.append(self._TCTRL_episode_summary.GetValue().strip())
1735 for candidate in episode_name_candidates:
1736 if candidate is None:
1737 continue
1738 epi_name = candidate.strip().replace('\r', '//').replace('\n', '//')
1739 break
1740
1741 dlg = wx.TextEntryDialog (
1742 parent = self,
1743 message = _('Enter a short working name for this new problem:'),
1744 caption = _('Creating a problem (episode) to save the notelet under ...'),
1745 defaultValue = epi_name,
1746 style = wx.OK | wx.CANCEL | wx.CENTRE
1747 )
1748 decision = dlg.ShowModal()
1749 if decision != wx.ID_OK:
1750 return None
1751
1752 epi_name = dlg.GetValue().strip()
1753 if epi_name == u'':
1754 gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note'))
1755 return None
1756
1757
1758 new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True)
1759 new_episode['summary'] = self._TCTRL_episode_summary.GetValue().strip()
1760 new_episode.save()
1761
1762 if self.problem is not None:
1763 issue = emr.problem2issue(self.problem)
1764 if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True):
1765 gmGuiHelpers.gm_show_warning (
1766 _(
1767 'The new episode:\n'
1768 '\n'
1769 ' "%s"\n'
1770 '\n'
1771 'will remain unassociated despite the editor\n'
1772 'having been invoked from the health issue:\n'
1773 '\n'
1774 ' "%s"'
1775 ) % (
1776 new_episode['description'],
1777 issue['description']
1778 ),
1779 _('saving progress note')
1780 )
1781
1782 return new_episode
1783
1784
1785
1787 for field in self.soap_fields:
1788 wx_expando.EVT_ETC_LAYOUT_NEEDED(field, field.GetId(), self._on_expando_needs_layout)
1789 wx_expando.EVT_ETC_LAYOUT_NEEDED(self._TCTRL_episode_summary, self._TCTRL_episode_summary.GetId(), self._on_expando_needs_layout)
1790
1792
1793
1794
1795
1796 self.FitInside()
1797
1798 if self.HasScrollbar(wx.VERTICAL):
1799
1800 expando = self.FindWindowById(evt.GetId())
1801 y_expando = expando.GetPositionTuple()[1]
1802 h_expando = expando.GetSizeTuple()[1]
1803 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1
1804 y_cursor = int(round((float(line_cursor) / expando.NumberOfLines) * h_expando))
1805 y_desired_visible = y_expando + y_cursor
1806
1807 y_view = self.ViewStart[1]
1808 h_view = self.GetClientSizeTuple()[1]
1809
1810
1811
1812
1813
1814
1815
1816
1817 if y_desired_visible < y_view:
1818
1819 self.Scroll(0, y_desired_visible)
1820
1821 if y_desired_visible > h_view:
1822
1823 self.Scroll(0, y_desired_visible)
1824
1825
1826
1828 soap_notes = []
1829
1830 tmp = self._TCTRL_Soap.GetValue().strip()
1831 if tmp != u'':
1832 soap_notes.append(['s', tmp])
1833
1834 tmp = self._TCTRL_sOap.GetValue().strip()
1835 if tmp != u'':
1836 soap_notes.append(['o', tmp])
1837
1838 tmp = self._TCTRL_soAp.GetValue().strip()
1839 if tmp != u'':
1840 soap_notes.append(['a', tmp])
1841
1842 tmp = self._TCTRL_soaP.GetValue().strip()
1843 if tmp != u'':
1844 soap_notes.append(['p', tmp])
1845
1846 return soap_notes
1847
1848 soap = property(_get_soap, lambda x:x)
1849
1851
1852
1853 for field in self.soap_fields:
1854 if field.GetValue().strip() != u'':
1855 return False
1856
1857
1858 summary = self._TCTRL_episode_summary.GetValue().strip()
1859 if self.problem is None:
1860 if summary != u'':
1861 return False
1862 elif self.problem['type'] == u'issue':
1863 if summary != u'':
1864 return False
1865 else:
1866 if self.problem['summary'] is None:
1867 if summary != u'':
1868 return False
1869 else:
1870 if summary != self.problem['summary'].strip():
1871 return False
1872
1873
1874 new_codes = self._PRW_episode_codes.GetData()
1875 if self.problem is None:
1876 if len(new_codes) > 0:
1877 return False
1878 elif self.problem['type'] == u'issue':
1879 if len(new_codes) > 0:
1880 return False
1881 else:
1882 old_code_pks = self.problem.generic_codes
1883 if len(old_code_pks) != len(new_codes):
1884 return False
1885 for code in new_codes:
1886 if code['data'] not in old_code_pks:
1887 return False
1888
1889 return True
1890
1891 empty = property(_get_empty, lambda x:x)
1892
1893 -class cSoapLineTextCtrl(wx_expando.ExpandoTextCtrl):
1894
1895 - def __init__(self, *args, **kwargs):
1896
1897 wx_expando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
1898
1899 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+")
1900
1901 self.__register_interests()
1902
1903
1904
1905 - def _wrapLine(self, line, dc, width):
1906
1907 if (wx.MAJOR_VERSION >= 2) and (wx.MINOR_VERSION > 8):
1908 return super(cSoapLineTextCtrl, self)._wrapLine(line, dc, width)
1909
1910
1911
1912
1913 pte = dc.GetPartialTextExtents(line)
1914 width -= wx.SystemSettings.GetMetric(wx.SYS_VSCROLL_X)
1915 idx = 0
1916 start = 0
1917 count = 0
1918 spc = -1
1919 while idx < len(pte):
1920 if line[idx] == ' ':
1921 spc = idx
1922 if pte[idx] - start > width:
1923
1924 count += 1
1925
1926 if spc != -1:
1927 idx = spc + 1
1928 spc = -1
1929 if idx < len(pte):
1930 start = pte[idx]
1931 else:
1932 idx += 1
1933 return count
1934
1935
1936
1938
1939
1940 wx.EVT_CHAR(self, self.__on_char)
1941 wx.EVT_SET_FOCUS(self, self.__on_focus)
1942
1943 - def __on_focus(self, evt):
1944 evt.Skip()
1945 wx.CallAfter(self._after_on_focus)
1946
1947 - def _after_on_focus(self):
1948
1949 evt = wx.PyCommandEvent(wx_expando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
1950 evt.SetEventObject(self)
1951
1952
1953
1954
1955 self.GetEventHandler().ProcessEvent(evt)
1956
1957 - def __on_char(self, evt):
1958 char = unichr(evt.GetUnicodeKey())
1959
1960 if self.LastPosition == 1:
1961 evt.Skip()
1962 return
1963
1964 explicit_expansion = False
1965 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT):
1966 if evt.GetKeyCode() != 13:
1967 evt.Skip()
1968 return
1969 explicit_expansion = True
1970
1971 if not explicit_expansion:
1972 if self.__keyword_separators.match(char) is None:
1973 evt.Skip()
1974 return
1975
1976 caret_pos, line_no = self.PositionToXY(self.InsertionPoint)
1977 line = self.GetLineText(line_no)
1978 word = self.__keyword_separators.split(line[:caret_pos])[-1]
1979
1980 if (
1981 (not explicit_expansion)
1982 and
1983 (word != u'$$steffi')
1984 and
1985 (word not in [ r[0] for r in gmPG2.get_text_expansion_keywords() ])
1986 ):
1987 evt.Skip()
1988 return
1989
1990 start = self.InsertionPoint - len(word)
1991 wx.CallAfter(self.replace_keyword_with_expansion, word, start, explicit_expansion)
1992
1993 evt.Skip()
1994 return
1995
1996 - def replace_keyword_with_expansion(self, keyword=None, position=None, show_list=False):
1997
1998 if show_list:
1999 candidates = gmPG2.get_keyword_expansion_candidates(keyword = keyword)
2000 if len(candidates) == 0:
2001 return
2002 if len(candidates) == 1:
2003 keyword = candidates[0]
2004 else:
2005 keyword = gmListWidgets.get_choices_from_list (
2006 parent = self,
2007 msg = _(
2008 'Several macros match the keyword [%s].\n'
2009 '\n'
2010 'Please select the expansion you want to happen.'
2011 ) % keyword,
2012 caption = _('Selecting text macro'),
2013 choices = candidates,
2014 columns = [_('Keyword')],
2015 single_selection = True,
2016 can_return_empty = False
2017 )
2018 if keyword is None:
2019 return
2020
2021 expansion = gmPG2.expand_keyword(keyword = keyword)
2022
2023 if expansion is None:
2024 return
2025
2026 if expansion == u'':
2027 return
2028
2029 self.Replace (
2030 position,
2031 position + len(keyword),
2032 expansion
2033 )
2034
2035 self.SetInsertionPoint(position + len(expansion) + 1)
2036 self.ShowPosition(position + len(expansion) + 1)
2037
2038 return
2039
2040
2041
2072
2073 cmd = gmCfgWidgets.configure_string_option (
2074 message = _(
2075 'Enter the shell command with which to start\n'
2076 'the image editor for visual progress notes.\n'
2077 '\n'
2078 'Any "%(img)s" included with the arguments\n'
2079 'will be replaced by the file name of the\n'
2080 'note template.'
2081 ),
2082 option = u'external.tools.visual_soap_editor_cmd',
2083 bias = 'user',
2084 default_value = None,
2085 validator = is_valid
2086 )
2087
2088 return cmd
2089
2091 if parent is None:
2092 parent = wx.GetApp().GetTopWindow()
2093
2094 dlg = wx.FileDialog (
2095 parent = parent,
2096 message = _('Choose file to use as template for new visual progress note'),
2097 defaultDir = os.path.expanduser('~'),
2098 defaultFile = '',
2099
2100 style = wx.OPEN | wx.HIDE_READONLY | wx.FILE_MUST_EXIST
2101 )
2102 result = dlg.ShowModal()
2103
2104 if result == wx.ID_CANCEL:
2105 dlg.Destroy()
2106 return None
2107
2108 full_filename = dlg.GetPath()
2109 dlg.Hide()
2110 dlg.Destroy()
2111 return full_filename
2112
2114
2115 if parent is None:
2116 parent = wx.GetApp().GetTopWindow()
2117
2118 dlg = gmGuiHelpers.c3ButtonQuestionDlg (
2119 parent,
2120 -1,
2121 caption = _('Visual progress note source'),
2122 question = _('From which source do you want to pick the image template ?'),
2123 button_defs = [
2124 {'label': _('Database'), 'tooltip': _('List of templates in the database.'), 'default': True},
2125 {'label': _('File'), 'tooltip': _('Files in the filesystem.'), 'default': False},
2126 {'label': _('Device'), 'tooltip': _('Image capture devices (scanners, cameras, etc)'), 'default': False}
2127 ]
2128 )
2129 result = dlg.ShowModal()
2130 dlg.Destroy()
2131
2132
2133 if result == wx.ID_YES:
2134 _log.debug('visual progress note template from: database template')
2135 from Gnumed.wxpython import gmFormWidgets
2136 template = gmFormWidgets.manage_form_templates (
2137 parent = parent,
2138 template_types = [gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE],
2139 active_only = True
2140 )
2141 if template is None:
2142 return (None, None)
2143 filename = template.export_to_file()
2144 if filename is None:
2145 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note template for [%s].') % template['name_long'])
2146 return (None, None)
2147 return (filename, True)
2148
2149
2150 if result == wx.ID_NO:
2151 _log.debug('visual progress note template from: disk file')
2152 fname = select_file_as_visual_progress_note_template(parent = parent)
2153 if fname is None:
2154 return (None, None)
2155
2156 ext = os.path.splitext(fname)[1]
2157 tmp_name = gmTools.get_unique_filename(suffix = ext)
2158 _log.debug('visual progress note from file: [%s] -> [%s]', fname, tmp_name)
2159 shutil.copy2(fname, tmp_name)
2160 return (tmp_name, False)
2161
2162
2163 if result == wx.ID_CANCEL:
2164 _log.debug('visual progress note template from: image capture device')
2165 fnames = gmDocumentWidgets.acquire_images_from_capture_device(device = None, calling_window = parent)
2166 if fnames is None:
2167 return (None, None)
2168 if len(fnames) == 0:
2169 return (None, None)
2170 return (fnames[0], False)
2171
2172 _log.debug('no visual progress note template source selected')
2173 return (None, None)
2174
2176 """This assumes <filename> contains an image which can be handled by the configured image editor."""
2177
2178 if doc_part is not None:
2179 filename = doc_part.export_to_file()
2180 if filename is None:
2181 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.'))
2182 return None
2183
2184 dbcfg = gmCfg.cCfgSQL()
2185 cmd = dbcfg.get2 (
2186 option = u'external.tools.visual_soap_editor_cmd',
2187 workplace = gmSurgery.gmCurrentPractice().active_workplace,
2188 bias = 'user'
2189 )
2190
2191 if cmd is None:
2192 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = False)
2193 cmd = configure_visual_progress_note_editor()
2194 if cmd is None:
2195 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = True)
2196 return None
2197
2198 if u'%(img)s' in cmd:
2199 cmd % {u'img': filename}
2200 else:
2201 cmd = u'%s %s' % (cmd, filename)
2202
2203 if discard_unmodified:
2204 original_stat = os.stat(filename)
2205 original_md5 = gmTools.file2md5(filename)
2206
2207 success = gmShellAPI.run_command_in_shell(cmd, blocking = True)
2208 if not success:
2209 gmGuiHelpers.gm_show_error (
2210 _(
2211 'There was a problem with running the editor\n'
2212 'for visual progress notes.\n'
2213 '\n'
2214 ' [%s]\n'
2215 '\n'
2216 ) % cmd,
2217 _('Editing visual progress note')
2218 )
2219 return None
2220
2221 try:
2222 open(filename, 'r').close()
2223 except StandardError:
2224 _log.exception('problem accessing visual progress note file [%s]', filename)
2225 gmGuiHelpers.gm_show_error (
2226 _(
2227 'There was a problem reading the visual\n'
2228 'progress note from the file:\n'
2229 '\n'
2230 ' [%s]\n'
2231 '\n'
2232 ) % filename,
2233 _('Saving visual progress note')
2234 )
2235 return None
2236
2237 if discard_unmodified:
2238 modified_stat = os.stat(filename)
2239
2240 if original_stat.st_size == modified_stat.st_size:
2241 modified_md5 = gmTools.file2md5(filename)
2242
2243 if original_md5 == modified_md5:
2244 _log.debug('visual progress note (template) not modified')
2245
2246 msg = _(
2247 u'You either created a visual progress note from a template\n'
2248 u'in the database (rather than from a file on disk) or you\n'
2249 u'edited an existing visual progress note.\n'
2250 u'\n'
2251 u'The template/original was not modified at all, however.\n'
2252 u'\n'
2253 u'Do you still want to save the unmodified image as a\n'
2254 u'visual progress note into the EMR of the patient ?\n'
2255 )
2256 save_unmodified = gmGuiHelpers.gm_show_question (
2257 msg,
2258 _('Saving visual progress note')
2259 )
2260 if not save_unmodified:
2261 _log.debug('user discarded unmodified note')
2262 return
2263
2264 if doc_part is not None:
2265 doc_part.update_data_from_file(fname = filename)
2266 doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2267 return None
2268
2269 if not isinstance(episode, gmEMRStructItems.cEpisode):
2270 if episode is None:
2271 episode = _('visual progress notes')
2272 pat = gmPerson.gmCurrentPatient()
2273 emr = pat.get_emr()
2274 episode = emr.add_episode(episode_name = episode.strip(), pk_health_issue = health_issue, is_open = False)
2275
2276 doc = gmDocumentWidgets.save_file_as_new_document (
2277 filename = filename,
2278 document_type = gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE,
2279 episode = episode,
2280 unlock_patient = True
2281 )
2282 doc.set_reviewed(technically_abnormal = False, clinically_relevant = True)
2283
2284 return doc
2285
2287 """Phrasewheel to allow selection of visual SOAP template."""
2288
2290
2291 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
2292
2293 query = u"""
2294 SELECT
2295 pk AS data,
2296 name_short AS list_label,
2297 name_sort AS field_label
2298 FROM
2299 ref.paperwork_templates
2300 WHERE
2301 fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND (
2302 name_long %%(fragment_condition)s
2303 OR
2304 name_short %%(fragment_condition)s
2305 )
2306 ORDER BY list_label
2307 LIMIT 15
2308 """ % gmDocuments.DOCUMENT_TYPE_VISUAL_PROGRESS_NOTE
2309
2310 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
2311 mp.setThresholds(2, 3, 5)
2312
2313 self.matcher = mp
2314 self.selection_only = True
2315
2321
2322 from Gnumed.wxGladeWidgets import wxgVisualSoapPresenterPnl
2323
2325
2330
2331
2332
2333 - def refresh(self, document_folder=None, episodes=None, encounter=None):
2334
2335 self.clear()
2336 if document_folder is not None:
2337 soap_docs = document_folder.get_visual_progress_notes(episodes = episodes, encounter = encounter)
2338 if len(soap_docs) > 0:
2339 for soap_doc in soap_docs:
2340 parts = soap_doc.parts
2341 if len(parts) == 0:
2342 continue
2343 part = parts[0]
2344 fname = part.export_to_file()
2345 if fname is None:
2346 continue
2347
2348
2349 img = gmGuiHelpers.file2scaled_image (
2350 filename = fname,
2351 height = 30
2352 )
2353
2354 bmp = wx_genstatbmp.GenStaticBitmap(self, -1, img, style = wx.NO_BORDER)
2355
2356
2357 img = gmGuiHelpers.file2scaled_image (
2358 filename = fname,
2359 height = 150
2360 )
2361 tip = agw_stt.SuperToolTip (
2362 u'',
2363 bodyImage = img,
2364 header = _('Created: %s') % part['date_generated'].strftime('%Y %B %d').decode(gmI18N.get_encoding()),
2365 footer = gmTools.coalesce(part['doc_comment'], u'').strip()
2366 )
2367 tip.SetTopGradientColor('white')
2368 tip.SetMiddleGradientColor('white')
2369 tip.SetBottomGradientColor('white')
2370 tip.SetTarget(bmp)
2371
2372 bmp.doc_part = part
2373 bmp.Bind(wx.EVT_LEFT_UP, self._on_bitmap_leftclicked)
2374
2375 self._SZR_soap.Add(bmp, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM | wx.EXPAND, 3)
2376 self.__bitmaps.append(bmp)
2377
2378 self.GetParent().Layout()
2379
2381 for child_idx in range(len(self._SZR_soap.GetChildren())):
2382 self._SZR_soap.Detach(child_idx)
2383 for bmp in self.__bitmaps:
2384 bmp.Destroy()
2385 self.__bitmaps = []
2386
2388 wx.CallAfter (
2389 edit_visual_progress_note,
2390 doc_part = evt.GetEventObject().doc_part,
2391 discard_unmodified = True
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
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634 if __name__ == '__main__':
2635
2636 if len(sys.argv) < 2:
2637 sys.exit()
2638
2639 if sys.argv[1] != 'test':
2640 sys.exit()
2641
2642 gmI18N.activate_locale()
2643 gmI18N.install_domain(domain = 'gnumed')
2644
2645
2654
2661
2674
2675
2676 test_cSoapNoteExpandoEditAreaPnl()
2677
2678
2679
2680