| Home | Trees | Indices | Help |
|
|---|
|
|
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 wxexpando
11
12
13 if __name__ == '__main__':
14 sys.path.insert(0, '../../')
15 from Gnumed.pycommon import gmI18N, gmDispatcher, gmTools, gmDateTime
16 from Gnumed.pycommon import gmShellAPI, gmPG2, gmCfg, gmMatchProvider
17 from Gnumed.business import gmPerson, gmEMRStructItems, gmClinNarrative, gmSurgery
18 from Gnumed.business import gmForms, gmDocuments, gmPersonSearch
19 from Gnumed.wxpython import gmListWidgets, gmEMRStructWidgets, gmRegetMixin
20 from Gnumed.wxpython import gmPhraseWheel, gmGuiHelpers, gmPatSearchWidgets
21 from Gnumed.wxpython import gmCfgWidgets, gmDocumentWidgets
22 from Gnumed.exporters import gmPatientExporter
23
24
25 _log = logging.getLogger('gm.ui')
26 _log.info(__version__)
27 #============================================================
28 # narrative related widgets/functions
29 #------------------------------------------------------------
30 -def move_progress_notes_to_another_encounter(parent=None, encounters=None, episodes=None, patient=None, move_all=False):
31
32 # sanity checks
33 if patient is None:
34 patient = gmPerson.gmCurrentPatient()
35
36 if not patient.connected:
37 gmDispatcher.send(signal = 'statustext', msg = _('Cannot move progress notes. No active patient.'))
38 return False
39
40 if parent is None:
41 parent = wx.GetApp().GetTopWindow()
42
43 emr = patient.get_emr()
44
45 if encounters is None:
46 encs = emr.get_encounters(episodes = episodes)
47 encounters = gmEMRStructWidgets.select_encounters (
48 parent = parent,
49 patient = patient,
50 single_selection = False,
51 encounters = encs
52 )
53
54 notes = emr.get_clin_narrative (
55 encounters = encounters,
56 episodes = episodes
57 )
58
59 # which narrative
60 if move_all:
61 selected_narr = notes
62 else:
63 selected_narr = gmListWidgets.get_choices_from_list (
64 parent = parent,
65 caption = _('Moving progress notes between encounters ...'),
66 single_selection = False,
67 can_return_empty = True,
68 data = notes,
69 msg = _('\n Select the progress notes to move from the list !\n\n'),
70 columns = [_('when'), _('who'), _('type'), _('entry')],
71 choices = [
72 [ narr['date'].strftime('%x %H:%M'),
73 narr['provider'],
74 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
75 narr['narrative'].replace('\n', '/').replace('\r', '/')
76 ] for narr in notes
77 ]
78 )
79
80 if not selected_narr:
81 return True
82
83 # which encounter to move to
84 enc2move2 = gmEMRStructWidgets.select_encounters (
85 parent = parent,
86 patient = patient,
87 single_selection = True
88 )
89
90 if not enc2move2:
91 return True
92
93 for narr in selected_narr:
94 narr['pk_encounter'] = enc2move2['pk_encounter']
95 narr.save()
96
97 return True
98 #------------------------------------------------------------
100
101 # sanity checks
102 if patient is None:
103 patient = gmPerson.gmCurrentPatient()
104
105 if not patient.connected:
106 gmDispatcher.send(signal = 'statustext', msg = _('Cannot edit progress notes. No active patient.'))
107 return False
108
109 if parent is None:
110 parent = wx.GetApp().GetTopWindow()
111
112 emr = patient.get_emr()
113 #--------------------------
114 def delete(item):
115 if item is None:
116 return False
117 dlg = gmGuiHelpers.c2ButtonQuestionDlg (
118 parent,
119 -1,
120 caption = _('Deleting progress note'),
121 question = _(
122 'Are you positively sure you want to delete this\n'
123 'progress note from the medical record ?\n'
124 '\n'
125 'Note that even if you chose to delete the entry it will\n'
126 'still be (invisibly) kept in the audit trail to protect\n'
127 'you from litigation because physical deletion is known\n'
128 'to be unlawful in some jurisdictions.\n'
129 ),
130 button_defs = (
131 {'label': _('Delete'), 'tooltip': _('Yes, delete the progress note.'), 'default': False},
132 {'label': _('Cancel'), 'tooltip': _('No, do NOT delete the progress note.'), 'default': True}
133 )
134 )
135 decision = dlg.ShowModal()
136
137 if decision != wx.ID_YES:
138 return False
139
140 gmClinNarrative.delete_clin_narrative(narrative = item['pk_narrative'])
141 return True
142 #--------------------------
143 def edit(item):
144 if item is None:
145 return False
146
147 dlg = gmGuiHelpers.cMultilineTextEntryDlg (
148 parent,
149 -1,
150 title = _('Editing progress note'),
151 msg = _('This is the original progress note:'),
152 data = item.format(left_margin = u' ', fancy = True),
153 text = item['narrative']
154 )
155 decision = dlg.ShowModal()
156
157 if decision != wx.ID_SAVE:
158 return False
159
160 val = dlg.value
161 dlg.Destroy()
162 if val.strip() == u'':
163 return False
164
165 item['narrative'] = val
166 item.save_payload()
167
168 return True
169 #--------------------------
170 def refresh(lctrl):
171 notes = emr.get_clin_narrative (
172 encounters = encounters,
173 episodes = episodes,
174 providers = [ gmPerson.gmCurrentProvider()['short_alias'] ]
175 )
176 lctrl.set_string_items(items = [
177 [ narr['date'].strftime('%x %H:%M'),
178 gmClinNarrative.soap_cat2l10n[narr['soap_cat']],
179 narr['narrative'].replace('\n', '/').replace('\r', '/')
180 ] for narr in notes
181 ])
182 lctrl.set_data(data = notes)
183 #--------------------------
184
185 gmListWidgets.get_choices_from_list (
186 parent = parent,
187 caption = _('Managing progress notes'),
188 msg = _(
189 '\n'
190 ' This list shows the progress notes by %s.\n'
191 '\n'
192 ) % gmPerson.gmCurrentProvider()['short_alias'],
193 columns = [_('when'), _('type'), _('entry')],
194 single_selection = True,
195 can_return_empty = False,
196 edit_callback = edit,
197 delete_callback = delete,
198 refresh_callback = refresh,
199 ignore_OK_button = True
200 )
201 #------------------------------------------------------------
203
204 if parent is None:
205 parent = wx.GetApp().GetTopWindow()
206
207 searcher = wx.TextEntryDialog (
208 parent = parent,
209 message = _('Enter (regex) term to search for across all EMRs:'),
210 caption = _('Text search across all EMRs'),
211 style = wx.OK | wx.CANCEL | wx.CENTRE
212 )
213 result = searcher.ShowModal()
214
215 if result != wx.ID_OK:
216 return
217
218 wx.BeginBusyCursor()
219 term = searcher.GetValue()
220 searcher.Destroy()
221 results = gmClinNarrative.search_text_across_emrs(search_term = term)
222 wx.EndBusyCursor()
223
224 if len(results) == 0:
225 gmGuiHelpers.gm_show_info (
226 _(
227 'Nothing found for search term:\n'
228 ' "%s"'
229 ) % term,
230 _('Search results')
231 )
232 return
233
234 items = [ [gmPerson.cIdentity(aPK_obj = r['pk_patient'])['description_gender'], r['narrative'], r['src_table']] for r in results ]
235
236 selected_patient = gmListWidgets.get_choices_from_list (
237 parent = parent,
238 caption = _('Search results for %s') % term,
239 choices = items,
240 columns = [_('Patient'), _('Match'), _('Match location')],
241 data = [ r['pk_patient'] for r in results ],
242 single_selection = True,
243 can_return_empty = False
244 )
245
246 if selected_patient is None:
247 return
248
249 wx.CallAfter(gmPatSearchWidgets.set_active_patient, patient = gmPerson.cIdentity(aPK_obj = selected_patient))
250 #------------------------------------------------------------
252
253 # sanity checks
254 if patient is None:
255 patient = gmPerson.gmCurrentPatient()
256
257 if not patient.connected:
258 gmDispatcher.send(signal = 'statustext', msg = _('Cannot search EMR. No active patient.'))
259 return False
260
261 if parent is None:
262 parent = wx.GetApp().GetTopWindow()
263
264 searcher = wx.TextEntryDialog (
265 parent = parent,
266 message = _('Enter search term:'),
267 caption = _('Text search of entire EMR of active patient'),
268 style = wx.OK | wx.CANCEL | wx.CENTRE
269 )
270 result = searcher.ShowModal()
271
272 if result != wx.ID_OK:
273 searcher.Destroy()
274 return False
275
276 wx.BeginBusyCursor()
277 val = searcher.GetValue()
278 searcher.Destroy()
279 emr = patient.get_emr()
280 rows = emr.search_narrative_simple(val)
281 wx.EndBusyCursor()
282
283 if len(rows) == 0:
284 gmGuiHelpers.gm_show_info (
285 _(
286 'Nothing found for search term:\n'
287 ' "%s"'
288 ) % val,
289 _('Search results')
290 )
291 return True
292
293 txt = u''
294 for row in rows:
295 txt += u'%s: %s\n' % (
296 row['soap_cat'],
297 row['narrative']
298 )
299
300 txt += u' %s: %s - %s %s\n' % (
301 _('Encounter'),
302 row['encounter_started'].strftime('%x %H:%M'),
303 row['encounter_ended'].strftime('%H:%M'),
304 row['encounter_type']
305 )
306 txt += u' %s: %s\n' % (
307 _('Episode'),
308 row['episode']
309 )
310 txt += u' %s: %s\n\n' % (
311 _('Health issue'),
312 row['health_issue']
313 )
314
315 msg = _(
316 'Search term was: "%s"\n'
317 '\n'
318 'Search results:\n\n'
319 '%s\n'
320 ) % (val, txt)
321
322 dlg = wx.MessageDialog (
323 parent = parent,
324 message = msg,
325 caption = _('Search results for %s') % val,
326 style = wx.OK | wx.STAY_ON_TOP
327 )
328 dlg.ShowModal()
329 dlg.Destroy()
330
331 return True
332 #------------------------------------------------------------
334
335 # sanity checks
336 pat = gmPerson.gmCurrentPatient()
337 if not pat.connected:
338 gmDispatcher.send(signal = 'statustext', msg = _('Cannot export EMR for Medistar. No active patient.'))
339 return False
340
341 if encounter is None:
342 encounter = pat.get_emr().active_encounter
343
344 if parent is None:
345 parent = wx.GetApp().GetTopWindow()
346
347 # get file name
348 aWildcard = "%s (*.txt)|*.txt|%s (*)|*" % (_("text files"), _("all files"))
349 # FIXME: make configurable
350 aDefDir = os.path.abspath(os.path.expanduser(os.path.join('~', 'gnumed','export')))
351 # FIXME: make configurable
352 fname = '%s-%s-%s-%s-%s.txt' % (
353 'Medistar-MD',
354 time.strftime('%Y-%m-%d',time.localtime()),
355 pat['lastnames'].replace(' ', '-'),
356 pat['firstnames'].replace(' ', '_'),
357 pat.get_formatted_dob(format = '%Y-%m-%d')
358 )
359 dlg = wx.FileDialog (
360 parent = parent,
361 message = _("Save EMR extract for MEDISTAR import as..."),
362 defaultDir = aDefDir,
363 defaultFile = fname,
364 wildcard = aWildcard,
365 style = wx.SAVE
366 )
367 choice = dlg.ShowModal()
368 fname = dlg.GetPath()
369 dlg.Destroy()
370 if choice != wx.ID_OK:
371 return False
372
373 wx.BeginBusyCursor()
374 _log.debug('exporting encounter for medistar import to [%s]', fname)
375 exporter = gmPatientExporter.cMedistarSOAPExporter()
376 successful, fname = exporter.export_to_file (
377 filename = fname,
378 encounter = encounter,
379 soap_cats = u'soap',
380 export_to_import_file = True
381 )
382 if not successful:
383 gmGuiHelpers.gm_show_error (
384 _('Error exporting progress notes for MEDISTAR import.'),
385 _('MEDISTAR progress notes export')
386 )
387 wx.EndBusyCursor()
388 return False
389
390 gmDispatcher.send(signal = 'statustext', msg = _('Successfully exported progress notes into file [%s] for Medistar import.') % fname, beep=False)
391
392 wx.EndBusyCursor()
393 return True
394 #------------------------------------------------------------
396 """soap_cats needs to be a list"""
397
398 pat = gmPerson.gmCurrentPatient()
399 emr = pat.get_emr()
400
401 if parent is None:
402 parent = wx.GetApp().GetTopWindow()
403
404 selected_soap = {}
405 selected_issue_pks = []
406 selected_episode_pks = []
407 selected_narrative_pks = []
408
409 while 1:
410 # 1) select health issues to select episodes from
411 all_issues = emr.get_health_issues()
412 all_issues.insert(0, gmEMRStructItems.get_dummy_health_issue())
413 dlg = gmEMRStructWidgets.cIssueListSelectorDlg (
414 parent = parent,
415 id = -1,
416 issues = all_issues,
417 msg = _('\n In the list below mark the health issues you want to report on.\n')
418 )
419 selection_idxs = []
420 for idx in range(len(all_issues)):
421 if all_issues[idx]['pk_health_issue'] in selected_issue_pks:
422 selection_idxs.append(idx)
423 if len(selection_idxs) != 0:
424 dlg.set_selections(selections = selection_idxs)
425 btn_pressed = dlg.ShowModal()
426 selected_issues = dlg.get_selected_item_data()
427 dlg.Destroy()
428
429 if btn_pressed == wx.ID_CANCEL:
430 return selected_soap.values()
431
432 selected_issue_pks = [ i['pk_health_issue'] for i in selected_issues ]
433
434 while 1:
435 # 2) select episodes to select items from
436 all_epis = emr.get_episodes(issues = selected_issue_pks)
437
438 if len(all_epis) == 0:
439 gmDispatcher.send(signal = 'statustext', msg = _('No episodes recorded for the health issues selected.'))
440 break
441
442 dlg = gmEMRStructWidgets.cEpisodeListSelectorDlg (
443 parent = parent,
444 id = -1,
445 episodes = all_epis,
446 msg = _(
447 '\n These are the episodes known for the health issues just selected.\n\n'
448 ' Now, mark the the episodes you want to report on.\n'
449 )
450 )
451 selection_idxs = []
452 for idx in range(len(all_epis)):
453 if all_epis[idx]['pk_episode'] in selected_episode_pks:
454 selection_idxs.append(idx)
455 if len(selection_idxs) != 0:
456 dlg.set_selections(selections = selection_idxs)
457 btn_pressed = dlg.ShowModal()
458 selected_epis = dlg.get_selected_item_data()
459 dlg.Destroy()
460
461 if btn_pressed == wx.ID_CANCEL:
462 break
463
464 selected_episode_pks = [ i['pk_episode'] for i in selected_epis ]
465
466 # 3) select narrative corresponding to the above constraints
467 all_narr = emr.get_clin_narrative(episodes = selected_episode_pks, soap_cats = soap_cats)
468
469 if len(all_narr) == 0:
470 gmDispatcher.send(signal = 'statustext', msg = _('No narrative available for selected episodes.'))
471 continue
472
473 dlg = cNarrativeListSelectorDlg (
474 parent = parent,
475 id = -1,
476 narrative = all_narr,
477 msg = _(
478 '\n This is the narrative (type %s) for the chosen episodes.\n\n'
479 ' Now, mark the entries you want to include in your report.\n'
480 ) % u'/'.join([ gmClinNarrative.soap_cat2l10n[cat] for cat in gmTools.coalesce(soap_cats, list(u'soap')) ])
481 )
482 selection_idxs = []
483 for idx in range(len(all_narr)):
484 if all_narr[idx]['pk_narrative'] in selected_narrative_pks:
485 selection_idxs.append(idx)
486 if len(selection_idxs) != 0:
487 dlg.set_selections(selections = selection_idxs)
488 btn_pressed = dlg.ShowModal()
489 selected_narr = dlg.get_selected_item_data()
490 dlg.Destroy()
491
492 if btn_pressed == wx.ID_CANCEL:
493 continue
494
495 selected_narrative_pks = [ i['pk_narrative'] for i in selected_narr ]
496 for narr in selected_narr:
497 selected_soap[narr['pk_narrative']] = narr
498 #------------------------------------------------------------
500
502
503 narrative = kwargs['narrative']
504 del kwargs['narrative']
505
506 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs)
507
508 self.SetTitle(_('Select the narrative you are interested in ...'))
509 # FIXME: add epi/issue
510 self._LCTRL_items.set_columns([_('when'), _('who'), _('type'), _('entry')]) #, _('Episode'), u'', _('Health Issue')])
511 # FIXME: date used should be date of encounter, not date_modified
512 self._LCTRL_items.set_string_items (
513 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 ]
514 )
515 self._LCTRL_items.set_column_widths()
516 self._LCTRL_items.set_data(data = narrative)
517 #------------------------------------------------------------
518 from Gnumed.wxGladeWidgets import wxgMoveNarrativeDlg
519
521
523
524 self.encounter = kwargs['encounter']
525 self.source_episode = kwargs['episode']
526 del kwargs['encounter']
527 del kwargs['episode']
528
529 wxgMoveNarrativeDlg.wxgMoveNarrativeDlg.__init__(self, *args, **kwargs)
530
531 self.LBL_source_episode.SetLabel(u'%s%s' % (self.source_episode['description'], gmTools.coalesce(self.source_episode['health_issue'], u'', u' (%s)')))
532 self.LBL_encounter.SetLabel('%s: %s %s - %s' % (
533 self.encounter['started'].strftime('%x').decode(gmI18N.get_encoding()),
534 self.encounter['l10n_type'],
535 self.encounter['started'].strftime('%H:%M'),
536 self.encounter['last_affirmed'].strftime('%H:%M')
537 ))
538 pat = gmPerson.gmCurrentPatient()
539 emr = pat.get_emr()
540 narr = emr.get_clin_narrative(episodes=[self.source_episode['pk_episode']], encounters=[self.encounter['pk_encounter']])
541 if len(narr) == 0:
542 narr = [{'narrative': _('There is no narrative for this episode in this encounter.')}]
543 self.LBL_narrative.SetLabel(u'\n'.join([n['narrative'] for n in narr]))
544
545 #------------------------------------------------------------
567 #============================================================
568 from Gnumed.wxGladeWidgets import wxgSoapPluginPnl
569
571 """A panel for in-context editing of progress notes.
572
573 Expects to be used as a notebook page.
574
575 Left hand side:
576 - problem list (health issues and active episodes)
577 - hints area
578
579 Right hand side:
580 - previous notes
581 - notebook with progress note editors
582 - encounter details fields
583 - visual soap area
584
585 Listens to patient change signals, thus acts on the current patient.
586 """
588
589 wxgSoapPluginPnl.wxgSoapPluginPnl.__init__(self, *args, **kwargs)
590 gmRegetMixin.cRegetOnPaintMixin.__init__(self)
591
592 self.__pat = gmPerson.gmCurrentPatient()
593 self.__init_ui()
594 self.__reset_ui_content()
595
596 self.__register_interests()
597 #--------------------------------------------------------
598 # public API
599 #--------------------------------------------------------
601
602 if not self.__encounter_valid_for_save():
603 return False
604
605 emr = self.__pat.get_emr()
606 enc = emr.active_encounter
607
608 enc['pk_type'] = self._PRW_encounter_type.GetData()
609 enc['started'] = self._PRW_encounter_start.GetData().get_pydt()
610 enc['last_affirmed'] = self._PRW_encounter_end.GetData().get_pydt()
611 rfe = self._TCTRL_rfe.GetValue().strip()
612 if len(rfe) == 0:
613 enc['reason_for_encounter'] = None
614 else:
615 enc['reason_for_encounter'] = rfe
616 aoe = self._TCTRL_aoe.GetValue().strip()
617 if len(aoe) == 0:
618 enc['assessment_of_encounter'] = None
619 else:
620 enc['assessment_of_encounter'] = aoe
621
622 enc.save_payload()
623
624 return True
625 #--------------------------------------------------------
626 # internal helpers
627 #--------------------------------------------------------
629 self._LCTRL_active_problems.set_columns([_('Last'), _('Problem'), _('Health issue')])
630 self._LCTRL_active_problems.set_string_items()
631
632 self._splitter_main.SetSashGravity(0.5)
633 self._splitter_left.SetSashGravity(0.5)
634 self._splitter_right.SetSashGravity(1.0)
635 self._splitter_soap.SetSashGravity(0.75)
636
637 splitter_size = self._splitter_main.GetSizeTuple()[0]
638 self._splitter_main.SetSashPosition(splitter_size * 3 / 10, True)
639
640 splitter_size = self._splitter_left.GetSizeTuple()[1]
641 self._splitter_left.SetSashPosition(splitter_size * 6 / 20, True)
642
643 splitter_size = self._splitter_right.GetSizeTuple()[1]
644 self._splitter_right.SetSashPosition(splitter_size * 15 / 20, True)
645
646 splitter_size = self._splitter_soap.GetSizeTuple()[0]
647 self._splitter_soap.SetSashPosition(splitter_size * 3 / 4, True)
648
649 self._NB_soap_editors.DeleteAllPages()
650 #--------------------------------------------------------
652 """
653 Clear all information from input panel
654 """
655 self._LCTRL_active_problems.set_string_items()
656
657 self._TCTRL_recent_notes.SetValue(u'')
658
659 self._PRW_encounter_type.SetText(suppress_smarts = True)
660 self._PRW_encounter_start.SetText(suppress_smarts = True)
661 self._PRW_encounter_end.SetText(suppress_smarts = True)
662 self._TCTRL_rfe.SetValue(u'')
663 self._TCTRL_aoe.SetValue(u'')
664
665 self._NB_soap_editors.DeleteAllPages()
666 self._NB_soap_editors.add_editor()
667
668 self._PNL_visual_soap.clear()
669
670 self._lbl_hints.SetLabel(u'')
671 #--------------------------------------------------------
673 self._PNL_visual_soap.refresh()
674 #--------------------------------------------------------
676 """Update health problems list.
677 """
678
679 self._LCTRL_active_problems.set_string_items()
680
681 emr = self.__pat.get_emr()
682 problems = emr.get_problems (
683 include_closed_episodes = self._CHBOX_show_closed_episodes.IsChecked(),
684 include_irrelevant_issues = self._CHBOX_irrelevant_issues.IsChecked()
685 )
686
687 list_items = []
688 active_problems = []
689 for problem in problems:
690 if not problem['problem_active']:
691 if not problem['is_potential_problem']:
692 continue
693
694 active_problems.append(problem)
695
696 if problem['type'] == 'issue':
697 issue = emr.problem2issue(problem)
698 last_encounter = emr.get_last_encounter(issue_id = issue['pk_health_issue'])
699 if last_encounter is None:
700 last = issue['modified_when'].strftime('%m/%Y')
701 else:
702 last = last_encounter['last_affirmed'].strftime('%m/%Y')
703
704 list_items.append([last, problem['problem'], gmTools.u_left_arrow])
705
706 elif problem['type'] == 'episode':
707 epi = emr.problem2episode(problem)
708 last_encounter = emr.get_last_encounter(episode_id = epi['pk_episode'])
709 if last_encounter is None:
710 last = epi['episode_modified_when'].strftime('%m/%Y')
711 else:
712 last = last_encounter['last_affirmed'].strftime('%m/%Y')
713
714 list_items.append ([
715 last,
716 problem['problem'],
717 gmTools.coalesce(initial = epi['health_issue'], instead = gmTools.u_diameter)
718 ])
719
720 self._LCTRL_active_problems.set_string_items(items = list_items)
721 self._LCTRL_active_problems.set_column_widths()
722 self._LCTRL_active_problems.set_data(data = active_problems)
723
724 showing_potential_problems = (
725 self._CHBOX_show_closed_episodes.IsChecked()
726 or
727 self._CHBOX_irrelevant_issues.IsChecked()
728 )
729 if showing_potential_problems:
730 self._SZR_problem_list_staticbox.SetLabel(_('%s (active+potential) problems') % len(list_items))
731 else:
732 self._SZR_problem_list_staticbox.SetLabel(_('%s active problems') % len(list_items))
733
734 return True
735 #--------------------------------------------------------
737 """This refreshes the recent-notes part."""
738
739 if problem is None:
740 soap = u''
741 caption = u'<?>'
742
743 elif problem['type'] == u'issue':
744 emr = self.__pat.get_emr()
745 soap = u''
746 caption = problem['problem'][:35]
747
748 prev_enc = emr.get_last_but_one_encounter(issue_id = problem['pk_health_issue'])
749 if prev_enc is not None:
750 soap += prev_enc.format (
751 with_soap = True,
752 with_docs = False,
753 with_tests = False,
754 patient = self.__pat,
755 issues = [ problem['pk_health_issue'] ],
756 fancy_header = False
757 )
758
759 tmp = emr.active_encounter.format_soap (
760 soap_cats = 'soap',
761 emr = emr,
762 issues = [ problem['pk_health_issue'] ],
763 )
764 if len(tmp) > 0:
765 soap += _('Current encounter:') + u'\n'
766 soap += u'\n'.join(tmp) + u'\n'
767
768 elif problem['type'] == u'episode':
769 emr = self.__pat.get_emr()
770 soap = u''
771 caption = problem['problem'][:35]
772
773 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_episode'])
774 if prev_enc is None:
775 if problem['pk_health_issue'] is not None:
776 prev_enc = emr.get_last_but_one_encounter(episode_id = problem['pk_health_issue'])
777 if prev_enc is not None:
778 soap += prev_enc.format (
779 with_soap = True,
780 with_docs = False,
781 with_tests = False,
782 patient = self.__pat,
783 issues = [ problem['pk_health_issue'] ],
784 fancy_header = False
785 )
786 else:
787 soap += prev_enc.format (
788 episodes = [ problem['pk_episode'] ],
789 with_soap = True,
790 with_docs = False,
791 with_tests = False,
792 patient = self.__pat,
793 fancy_header = False
794 )
795
796 tmp = emr.active_encounter.format_soap (
797 soap_cats = 'soap',
798 emr = emr,
799 issues = [ problem['pk_health_issue'] ],
800 )
801 if len(tmp) > 0:
802 soap += _('Current encounter:') + u'\n'
803 soap += u'\n'.join(tmp) + u'\n'
804
805 else:
806 soap = u''
807 caption = u'<?>'
808
809 self._TCTRL_recent_notes.SetValue(soap)
810 self._TCTRL_recent_notes.ShowPosition(self._TCTRL_recent_notes.GetLastPosition())
811 self._SZR_recent_notes_staticbox.SetLabel(_('Most recent notes on %s%s%s') % (
812 gmTools.u_left_double_angle_quote,
813 caption,
814 gmTools.u_right_double_angle_quote
815 ))
816
817 self._TCTRL_recent_notes.Refresh()
818
819 return True
820 #--------------------------------------------------------
822 """Update encounter fields.
823 """
824 emr = self.__pat.get_emr()
825 enc = emr.active_encounter
826 self._PRW_encounter_type.SetText(value = enc['l10n_type'], data = enc['pk_type'])
827
828 fts = gmDateTime.cFuzzyTimestamp (
829 timestamp = enc['started'],
830 accuracy = gmDateTime.acc_minutes
831 )
832 self._PRW_encounter_start.SetText(fts.format_accurately(), data=fts)
833
834 fts = gmDateTime.cFuzzyTimestamp (
835 timestamp = enc['last_affirmed'],
836 accuracy = gmDateTime.acc_minutes
837 )
838 self._PRW_encounter_end.SetText(fts.format_accurately(), data=fts)
839
840 self._TCTRL_rfe.SetValue(gmTools.coalesce(enc['reason_for_encounter'], u''))
841 self._TCTRL_aoe.SetValue(gmTools.coalesce(enc['assessment_of_encounter'], u''))
842
843 self._PRW_encounter_type.Refresh()
844 self._PRW_encounter_start.Refresh()
845 self._PRW_encounter_end.Refresh()
846 self._TCTRL_rfe.Refresh()
847 self._TCTRL_aoe.Refresh()
848 #--------------------------------------------------------
850 """Assumes that the field data is valid."""
851
852 emr = self.__pat.get_emr()
853 enc = emr.active_encounter
854
855 data = {
856 'pk_type': self._PRW_encounter_type.GetData(),
857 'reason_for_encounter': gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u''),
858 'assessment_of_encounter': gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
859 'pk_location': enc['pk_location'],
860 'pk_patient': enc['pk_patient']
861 }
862
863 if self._PRW_encounter_start.GetData() is None:
864 data['started'] = None
865 else:
866 data['started'] = self._PRW_encounter_start.GetData().get_pydt()
867
868 if self._PRW_encounter_end.GetData() is None:
869 data['last_affirmed'] = None
870 else:
871 data['last_affirmed'] = self._PRW_encounter_end.GetData().get_pydt()
872
873 return not enc.same_payload(another_object = data)
874 #--------------------------------------------------------
876
877 found_error = False
878
879 if self._PRW_encounter_type.GetData() is None:
880 found_error = True
881 msg = _('Cannot save encounter: missing type.')
882
883 if self._PRW_encounter_start.GetData() is None:
884 found_error = True
885 msg = _('Cannot save encounter: missing start time.')
886
887 if self._PRW_encounter_end.GetData() is None:
888 found_error = True
889 msg = _('Cannot save encounter: missing end time.')
890
891 if found_error:
892 gmDispatcher.send(signal = 'statustext', msg = msg, beep = True)
893 return False
894
895 return True
896 #--------------------------------------------------------
897 # event handling
898 #--------------------------------------------------------
900 """Configure enabled event signals."""
901 # client internal signals
902 gmDispatcher.connect(signal = u'pre_patient_selection', receiver = self._on_pre_patient_selection)
903 gmDispatcher.connect(signal = u'post_patient_selection', receiver = self._on_post_patient_selection)
904 gmDispatcher.connect(signal = u'episode_mod_db', receiver = self._on_episode_issue_mod_db)
905 gmDispatcher.connect(signal = u'health_issue_mod_db', receiver = self._on_episode_issue_mod_db)
906 gmDispatcher.connect(signal = u'doc_mod_db', receiver = self._on_doc_mod_db)
907 gmDispatcher.connect(signal = u'current_encounter_modified', receiver = self._on_current_encounter_modified)
908 gmDispatcher.connect(signal = u'current_encounter_switched', receiver = self._on_current_encounter_switched)
909
910 # synchronous signals
911 self.__pat.register_pre_selection_callback(callback = self._pre_selection_callback)
912 gmDispatcher.send(signal = u'register_pre_exit_callback', callback = self._pre_exit_callback)
913 #--------------------------------------------------------
915 """Another patient is about to be activated.
916
917 Patient change will not proceed before this returns True.
918 """
919 # don't worry about the encounter here - it will be offered
920 # for editing higher up if anything was saved to the EMR
921 if not self.__pat.connected:
922 return True
923 return self._NB_soap_editors.warn_on_unsaved_soap()
924 #--------------------------------------------------------
926 """The client is about to be shut down.
927
928 Shutdown will not proceed before this returns.
929 """
930 if not self.__pat.connected:
931 return True
932
933 # if self.__encounter_modified():
934 # do_save_enc = gmGuiHelpers.gm_show_question (
935 # aMessage = _(
936 # 'You have modified the details\n'
937 # 'of the current encounter.\n'
938 # '\n'
939 # 'Do you want to save those changes ?'
940 # ),
941 # aTitle = _('Starting new encounter')
942 # )
943 # if do_save_enc:
944 # if not self.save_encounter():
945 # gmDispatcher.send(signal = u'statustext', msg = _('Error saving current encounter.'), beep = True)
946
947 emr = self.__pat.get_emr()
948 saved = self._NB_soap_editors.save_all_editors (
949 emr = emr,
950 episode_name_candidates = [
951 gmTools.none_if(self._TCTRL_aoe.GetValue().strip(), u''),
952 gmTools.none_if(self._TCTRL_rfe.GetValue().strip(), u'')
953 ]
954 )
955 if not saved:
956 gmDispatcher.send(signal = 'statustext', msg = _('Cannot save all editors. Some were kept open.'), beep = True)
957 return True
958 #--------------------------------------------------------
961 #--------------------------------------------------------
964 #--------------------------------------------------------
967 #--------------------------------------------------------
970 #--------------------------------------------------------
973 #--------------------------------------------------------
976 #--------------------------------------------------------
979 #--------------------------------------------------------
983 #--------------------------------------------------------
987 #--------------------------------------------------------
989 """Show related note at the bottom."""
990 emr = self.__pat.get_emr()
991 self.__refresh_recent_notes (
992 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
993 )
994 #--------------------------------------------------------
996 """Open progress note editor for this problem.
997 """
998 problem = self._LCTRL_active_problems.get_selected_item_data(only_one = True)
999 if problem is None:
1000 return True
1001
1002 dbcfg = gmCfg.cCfgSQL()
1003 allow_duplicate_editors = bool(dbcfg.get2 (
1004 option = u'horstspace.soap_editor.allow_same_episode_multiple_times',
1005 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1006 bias = u'user',
1007 default = False
1008 ))
1009 if self._NB_soap_editors.add_editor(problem = problem, allow_same_problem = allow_duplicate_editors):
1010 return True
1011
1012 gmGuiHelpers.gm_show_error (
1013 aMessage = _(
1014 'Cannot open progress note editor for\n\n'
1015 '[%s].\n\n'
1016 ) % problem['problem'],
1017 aTitle = _('opening progress note editor')
1018 )
1019 event.Skip()
1020 return False
1021 #--------------------------------------------------------
1025 #--------------------------------------------------------
1029 #--------------------------------------------------------
1033 #--------------------------------------------------------
1042 #--------------------------------------------------------
1054 #--------------------------------------------------------
1058 #--------------------------------------------------------
1069 #--------------------------------------------------------
1093 #--------------------------------------------------------
1096 #--------------------------------------------------------
1099 #--------------------------------------------------------
1100 # reget mixin API
1101 #--------------------------------------------------------
1107 #============================================================
1109 """A notebook holding panels with progress note editors.
1110
1111 There can be one or several progress note editor panel
1112 for each episode being worked on. The editor class in
1113 each panel is configurable.
1114
1115 There will always be one open editor.
1116 """
1118
1119 kwargs['style'] = wx.NB_TOP | wx.NB_MULTILINE | wx.NO_BORDER
1120
1121 wx.Notebook.__init__(self, *args, **kwargs)
1122 #--------------------------------------------------------
1123 # public API
1124 #--------------------------------------------------------
1126 """Add a progress note editor page.
1127
1128 The way <allow_same_problem> is currently used in callers
1129 it only applies to unassociated episodes.
1130 """
1131 problem_to_add = problem
1132
1133 # determine label
1134 if problem_to_add is None:
1135 label = _('new problem')
1136 else:
1137 # normalize problem type
1138 if isinstance(problem_to_add, gmEMRStructItems.cEpisode):
1139 problem_to_add = gmEMRStructItems.episode2problem(episode = problem_to_add)
1140
1141 elif isinstance(problem_to_add, gmEMRStructItems.cHealthIssue):
1142 problem_to_add = gmEMRStructItems.health_issue2problem(episode = problem_to_add)
1143
1144 if not isinstance(problem_to_add, gmEMRStructItems.cProblem):
1145 raise TypeError('cannot open progress note editor for [%s]' % problem_to_add)
1146
1147 label = problem_to_add['problem']
1148 # FIXME: configure maximum length
1149 if len(label) > 23:
1150 label = label[:21] + gmTools.u_ellipsis
1151
1152 # new unassociated problem or dupes allowed
1153 if (problem_to_add is None) or allow_same_problem:
1154 new_page = cSoapNoteExpandoEditAreaPnl(parent = self, id = -1, problem = problem_to_add)
1155 result = self.AddPage (
1156 page = new_page,
1157 text = label,
1158 select = True
1159 )
1160 return result
1161
1162 # real problem, no dupes allowed
1163 # - raise existing editor
1164 for page_idx in range(self.GetPageCount()):
1165 page = self.GetPage(page_idx)
1166
1167 # editor is for unassociated new problem
1168 if page.problem is None:
1169 continue
1170
1171 # editor is for episode
1172 if page.problem['type'] == 'episode':
1173 if page.problem['pk_episode'] == problem_to_add['pk_episode']:
1174 self.SetSelection(page_idx)
1175 gmDispatcher.send(signal = u'statustext', msg = u'Raising existing editor.', beep = True)
1176 return True
1177 continue
1178
1179 # editor is for health issue
1180 if page.problem['type'] == 'issue':
1181 if page.problem['pk_health_issue'] == problem_to_add['pk_health_issue']:
1182 self.SetSelection(page_idx)
1183 gmDispatcher.send(signal = u'statustext', msg = u'Raising existing editor.', beep = True)
1184 return True
1185 continue
1186
1187 # - or add new editor
1188 new_page = cSoapNoteExpandoEditAreaPnl(parent = self, id = -1, problem = problem_to_add)
1189 result = self.AddPage (
1190 page = new_page,
1191 text = label,
1192 select = True
1193 )
1194
1195 return result
1196 #--------------------------------------------------------
1198
1199 page_idx = self.GetSelection()
1200 page = self.GetPage(page_idx)
1201
1202 if not page.empty:
1203 really_discard = gmGuiHelpers.gm_show_question (
1204 _('Are you sure you really want to\n'
1205 'discard this progress note ?\n'
1206 ),
1207 _('Discarding progress note')
1208 )
1209 if really_discard is False:
1210 return
1211
1212 self.DeletePage(page_idx)
1213
1214 # always keep one unassociated editor open
1215 if self.GetPageCount() == 0:
1216 self.add_editor()
1217 #--------------------------------------------------------
1219
1220 page_idx = self.GetSelection()
1221 page = self.GetPage(page_idx)
1222
1223 if not page.save(emr = emr, episode_name_candidates = episode_name_candidates):
1224 return
1225
1226 self.DeletePage(page_idx)
1227
1228 # always keep one unassociated editor open
1229 if self.GetPageCount() == 0:
1230 self.add_editor()
1231 #--------------------------------------------------------
1233 for page_idx in range(self.GetPageCount()):
1234 page = self.GetPage(page_idx)
1235 if page.empty:
1236 continue
1237
1238 gmGuiHelpers.gm_show_warning (
1239 _('There are unsaved progress notes !\n'),
1240 _('Unsaved progress notes')
1241 )
1242 return False
1243
1244 return True
1245 #--------------------------------------------------------
1247
1248 _log.debug('saving editors: %s', self.GetPageCount())
1249
1250 all_closed = True
1251 for page_idx in range((self.GetPageCount() - 1), 0, -1):
1252 _log.debug('#%s of %s', page_idx, self.GetPageCount())
1253 try:
1254 self.ChangeSelection(page_idx)
1255 _log.debug('editor raised')
1256 except:
1257 _log.exception('cannot raise editor')
1258 page = self.GetPage(page_idx)
1259 if page.save(emr = emr, episode_name_candidates = episode_name_candidates):
1260 _log.debug('saved, deleting now')
1261 self.DeletePage(page_idx)
1262 else:
1263 _log.debug('not saved, not deleting')
1264 all_closed = False
1265
1266 # always keep one unassociated editor open
1267 if self.GetPageCount() == 0:
1268 self.add_editor()
1269
1270 return (all_closed is True)
1271 #--------------------------------------------------------
1276 #--------------------------------------------------------
1281 #============================================================
1282 from Gnumed.wxGladeWidgets import wxgSoapNoteExpandoEditAreaPnl
1283
1284 -class cSoapNoteExpandoEditAreaPnl(wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl):
1285
1287
1288 try:
1289 self.problem = kwargs['problem']
1290 del kwargs['problem']
1291 except KeyError:
1292 self.problem = None
1293
1294 wxgSoapNoteExpandoEditAreaPnl.wxgSoapNoteExpandoEditAreaPnl.__init__(self, *args, **kwargs)
1295
1296 self.fields = [
1297 self._TCTRL_Soap,
1298 self._TCTRL_sOap,
1299 self._TCTRL_soAp,
1300 self._TCTRL_soaP
1301 ]
1302
1303 self.__register_interests()
1304 #--------------------------------------------------------
1308 #--------------------------------------------------------
1310
1311 if self.empty:
1312 return True
1313
1314 # new unassociated episode
1315 if (self.problem is None) or (self.problem['type'] == 'issue'):
1316
1317 episode_name_candidates.append(u'')
1318 for candidate in episode_name_candidates:
1319 if candidate is None:
1320 continue
1321 epi_name = candidate.strip().replace('\r', '//').replace('\n', '//')
1322 break
1323
1324 dlg = wx.TextEntryDialog (
1325 parent = self,
1326 message = _('Enter a short working name for this new problem:'),
1327 caption = _('Creating a problem (episode) to save the notelet under ...'),
1328 defaultValue = epi_name,
1329 style = wx.OK | wx.CANCEL | wx.CENTRE
1330 )
1331 decision = dlg.ShowModal()
1332 if decision != wx.ID_OK:
1333 return False
1334
1335 epi_name = dlg.GetValue().strip()
1336 if epi_name == u'':
1337 gmGuiHelpers.gm_show_error(_('Cannot save a new problem without a name.'), _('saving progress note'))
1338 return False
1339
1340 # create episode
1341 new_episode = emr.add_episode(episode_name = epi_name[:45], pk_health_issue = None, is_open = True)
1342
1343 if self.problem is not None:
1344 issue = emr.problem2issue(self.problem)
1345 if not gmEMRStructWidgets.move_episode_to_issue(episode = new_episode, target_issue = issue, save_to_backend = True):
1346 gmGuiHelpers.gm_show_warning (
1347 _(
1348 'The new episode:\n'
1349 '\n'
1350 ' "%s"\n'
1351 '\n'
1352 'will remain unassociated despite the editor\n'
1353 'having been invoked from the health issue:\n'
1354 '\n'
1355 ' "%s"'
1356 ) % (
1357 new_episode['description'],
1358 issue['description']
1359 ),
1360 _('saving progress note')
1361 )
1362
1363 epi_id = new_episode['pk_episode']
1364 else:
1365 epi_id = self.problem['pk_episode']
1366
1367 emr.add_notes(notes = self.soap, episode = epi_id)
1368
1369 return True
1370 #--------------------------------------------------------
1371 # event handling
1372 #--------------------------------------------------------
1374 for field in self.fields:
1375 wxexpando.EVT_ETC_LAYOUT_NEEDED(field, field.GetId(), self._on_expando_needs_layout)
1376 #--------------------------------------------------------
1378 # need to tell ourselves to re-Layout to refresh scroll bars
1379
1380 # provoke adding scrollbar if needed
1381 self.Fit()
1382
1383 if self.HasScrollbar(wx.VERTICAL):
1384 # scroll panel to show cursor
1385 expando = self.FindWindowById(evt.GetId())
1386 y_expando = expando.GetPositionTuple()[1]
1387 h_expando = expando.GetSizeTuple()[1]
1388 line_cursor = expando.PositionToXY(expando.GetInsertionPoint())[1] + 1
1389 y_cursor = int(round((float(line_cursor) / expando.NumberOfLines) * h_expando))
1390 y_desired_visible = y_expando + y_cursor
1391
1392 y_view = self.ViewStart[1]
1393 h_view = self.GetClientSizeTuple()[1]
1394
1395 # print "expando:", y_expando, "->", h_expando, ", lines:", expando.NumberOfLines
1396 # print "cursor :", y_cursor, "at line", line_cursor, ", insertion point:", expando.GetInsertionPoint()
1397 # print "wanted :", y_desired_visible
1398 # print "view-y :", y_view
1399 # print "scroll2:", h_view
1400
1401 # expando starts before view
1402 if y_desired_visible < y_view:
1403 # print "need to scroll up"
1404 self.Scroll(0, y_desired_visible)
1405
1406 if y_desired_visible > h_view:
1407 # print "need to scroll down"
1408 self.Scroll(0, y_desired_visible)
1409 #--------------------------------------------------------
1410 # properties
1411 #--------------------------------------------------------
1413 note = []
1414
1415 tmp = self._TCTRL_Soap.GetValue().strip()
1416 if tmp != u'':
1417 note.append(['s', tmp])
1418
1419 tmp = self._TCTRL_sOap.GetValue().strip()
1420 if tmp != u'':
1421 note.append(['o', tmp])
1422
1423 tmp = self._TCTRL_soAp.GetValue().strip()
1424 if tmp != u'':
1425 note.append(['a', tmp])
1426
1427 tmp = self._TCTRL_soaP.GetValue().strip()
1428 if tmp != u'':
1429 note.append(['p', tmp])
1430
1431 return note
1432
1433 soap = property(_get_soap, lambda x:x)
1434 #--------------------------------------------------------
1436 for field in self.fields:
1437 if field.GetValue().strip() != u'':
1438 return False
1439 return True
1440
1441 empty = property(_get_empty, lambda x:x)
1442 #============================================================
1444
1446
1447 wxexpando.ExpandoTextCtrl.__init__(self, *args, **kwargs)
1448
1449 self.__keyword_separators = regex.compile("[!?'\".,:;)}\]\r\n\s\t]+")
1450
1451 self.__register_interests()
1452 #------------------------------------------------
1453 # event handling
1454 #------------------------------------------------
1456 #wx.EVT_KEY_DOWN (self, self.__on_key_down)
1457 #wx.EVT_KEY_UP (self, self.__OnKeyUp)
1458 wx.EVT_CHAR(self, self.__on_char)
1459 wx.EVT_SET_FOCUS(self, self.__on_focus)
1460 #--------------------------------------------------------
1464 #--------------------------------------------------------
1466 evt = wx.PyCommandEvent(wxexpando.wxEVT_ETC_LAYOUT_NEEDED, self.GetId())
1467 evt.SetEventObject(self)
1468 evt.height = None
1469 evt.numLines = None
1470 self.GetEventHandler().ProcessEvent(evt)
1471 #--------------------------------------------------------
1473 char = unichr(evt.GetUnicodeKey())
1474
1475 if self.LastPosition == 1:
1476 evt.Skip()
1477 return
1478
1479 explicit_expansion = False
1480 if evt.GetModifiers() == (wx.MOD_CMD | wx.MOD_ALT): # portable CTRL-ALT-...
1481 if evt.GetKeyCode() != 13:
1482 evt.Skip()
1483 return
1484 explicit_expansion = True
1485
1486 if not explicit_expansion:
1487 if self.__keyword_separators.match(char) is None:
1488 evt.Skip()
1489 return
1490
1491 caret_pos, line_no = self.PositionToXY(self.InsertionPoint)
1492 line = self.GetLineText(line_no)
1493 word = self.__keyword_separators.split(line[:caret_pos])[-1]
1494
1495 if (
1496 (not explicit_expansion)
1497 and
1498 (word != u'$$steffi') # Easter Egg ;-)
1499 and
1500 (word not in [ r[0] for r in gmPG2.get_text_expansion_keywords() ])
1501 ):
1502 evt.Skip()
1503 return
1504
1505 start = self.InsertionPoint - len(word)
1506 wx.CallAfter(self.replace_keyword_with_expansion, word, start, explicit_expansion)
1507
1508 evt.Skip()
1509 return
1510 #------------------------------------------------
1512
1513 if show_list:
1514 candidates = gmPG2.get_keyword_expansion_candidates(keyword = keyword)
1515 if len(candidates) == 0:
1516 return
1517 if len(candidates) == 1:
1518 keyword = candidates[0]
1519 else:
1520 keyword = gmListWidgets.get_choices_from_list (
1521 parent = self,
1522 msg = _(
1523 'Several macros match the keyword [%s].\n'
1524 '\n'
1525 'Please select the expansion you want to happen.'
1526 ) % keyword,
1527 caption = _('Selecting text macro'),
1528 choices = candidates,
1529 columns = [_('Keyword')],
1530 single_selection = True,
1531 can_return_empty = False
1532 )
1533 if keyword is None:
1534 return
1535
1536 expansion = gmPG2.expand_keyword(keyword = keyword)
1537
1538 if expansion is None:
1539 return
1540
1541 if expansion == u'':
1542 return
1543
1544 self.Replace (
1545 position,
1546 position + len(keyword),
1547 expansion
1548 )
1549
1550 self.SetInsertionPoint(position + len(expansion) + 1)
1551 self.ShowPosition(position + len(expansion) + 1)
1552
1553 return
1554 #============================================================
1555 # visual progress notes
1556 #============================================================
1557 visual_progress_note_document_type = u'visual progress note'
1558
1559 #============================================================
1561
1562 def is_valid(value):
1563
1564 if value is None:
1565 gmDispatcher.send (
1566 signal = 'statustext',
1567 msg = _('You need to actually set an editor.'),
1568 beep = True
1569 )
1570 return False, value
1571
1572 if value.strip() == u'':
1573 gmDispatcher.send (
1574 signal = 'statustext',
1575 msg = _('You need to actually set an editor.'),
1576 beep = True
1577 )
1578 return False, value
1579
1580 found, binary = gmShellAPI.detect_external_binary(value)
1581 if not found:
1582 gmDispatcher.send (
1583 signal = 'statustext',
1584 msg = _('The command [%s] is not found.') % value,
1585 beep = True
1586 )
1587 return True, value
1588
1589 return True, binary
1590 #------------------------------------------
1591 gmCfgWidgets.configure_string_option (
1592 message = _(
1593 'Enter the shell command with which to start\n'
1594 'the image editor for visual progress notes.\n'
1595 '\n'
1596 'Any "%(img)s" included with the arguments\n'
1597 'will be replaced by the file name of the\n'
1598 'note template.'
1599 ),
1600 option = u'external.tools.visual_soap_editor_cmd',
1601 bias = 'user',
1602 default_value = None,
1603 validator = is_valid
1604 )
1605 #============================================================
1606 -def edit_visual_progress_note(filename=None, episode=None, discard_unmodified=False, doc_part=None):
1607 """This assumes <filename> contains an image which can be handled by the configured image editor."""
1608
1609 if doc_part is not None:
1610 filename = doc_part.export_to_file()
1611 if filename is None:
1612 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.'))
1613 return None
1614
1615 dbcfg = gmCfg.cCfgSQL()
1616 cmd = dbcfg.get2 (
1617 option = u'external.tools.visual_soap_editor_cmd',
1618 workplace = gmSurgery.gmCurrentPractice().active_workplace,
1619 bias = 'user'
1620 )
1621
1622 if cmd is None:
1623 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = False)
1624 cmd = configure_visual_progress_note_editor()
1625 if cmd is None:
1626 gmDispatcher.send(signal = u'statustext', msg = _('Editor for visual progress note not configured.'), beep = True)
1627 return None
1628
1629 if u'%(img)s' in cmd:
1630 cmd % {u'img': filename}
1631 else:
1632 cmd = u'%s %s' % (cmd, filename)
1633
1634 if discard_unmodified:
1635 original_stat = os.stat(filename)
1636 original_md5 = gmTools.file2md5(filename)
1637
1638 success = gmShellAPI.run_command_in_shell(cmd, blocking = True)
1639 if not success:
1640 gmGuiHelpers.gm_show_error (
1641 _(
1642 'There was a problem with running the editor\n'
1643 'for visual progress notes.\n'
1644 '\n'
1645 ' [%s]\n'
1646 '\n'
1647 ) % cmd,
1648 _('Editing visual progress note')
1649 )
1650 return None
1651
1652 try:
1653 open(filename, 'r').close()
1654 except StandardError:
1655 _log.exception('problem accessing visual progress note file [%s]', filename)
1656 gmGuiHelpers.gm_show_error (
1657 _(
1658 'There was a problem reading the visual\n'
1659 'progress note from the file:\n'
1660 '\n'
1661 ' [%s]\n'
1662 '\n'
1663 ) % filename,
1664 _('Saving visual progress note')
1665 )
1666 return None
1667
1668 if discard_unmodified:
1669 modified_stat = os.stat(filename)
1670 # same size ?
1671 if original_stat.st_size == modified_stat.st_size:
1672 modified_md5 = gmTools.file2md5(filename)
1673 # same hash ?
1674 if original_md5 == modified_md5:
1675 _log.debug('visual progress note (template) not modified')
1676 # ask user to decide
1677 msg = _(
1678 u'This visual progress note was created from a\n'
1679 u'template in the database rather than from a file\n'
1680 u'but the image was not modified at all.\n'
1681 u'\n'
1682 u'Do you want to still save the unmodified\n'
1683 u'image as a visual progress note into the\n'
1684 u'EMR of the patient ?'
1685 )
1686 save_unmodified = gmGuiHelpers.gm_show_question (
1687 msg,
1688 _('Saving visual progress note')
1689 )
1690 if not save_unmodified:
1691 _log.debug('user discarded unmodified note')
1692 return
1693
1694 if doc_part is not None:
1695 doc_part.update_data_from_file(fname = filename)
1696 doc_part.set_reviewed(technically_abnormal = False, clinically_relevant = True)
1697 return None
1698
1699 if not isinstance(episode, gmEMRStructItems.cEpisode):
1700 pat = gmPerson.gmCurrentPatient()
1701 emr = pat.get_emr()
1702 episode = emr.add_episode(episode_name = episode.strip(), is_open = False)
1703
1704 doc = gmDocumentWidgets.save_file_as_new_document (
1705 filename = filename,
1706 document_type = visual_progress_note_document_type,
1707 episode = episode,
1708 unlock_patient = True
1709 )
1710 doc.set_reviewed(technically_abnormal = False, clinically_relevant = True)
1711
1712 return doc
1713 #============================================================
1715 """Phrasewheel to allow selection of visual SOAP template."""
1716
1718
1719 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
1720
1721 query = u"""
1722 SELECT
1723 pk,
1724 name_short
1725 FROM
1726 ref.paperwork_templates
1727 WHERE
1728 fk_template_type = (SELECT pk FROM ref.form_types WHERE name = '%s') AND (
1729 name_long %%(fragment_condition)s
1730 OR
1731 name_short %%(fragment_condition)s
1732 )
1733 ORDER BY name_short
1734 LIMIT 15
1735 """ % visual_progress_note_document_type
1736
1737 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
1738 mp.setThresholds(2, 3, 5)
1739
1740 self.matcher = mp
1741 self.selection_only = True
1742 #--------------------------------------------------------
1744 if self.data is None:
1745 return None
1746
1747 return gmForms.cFormTemplate(aPK_obj = self.data)
1748 #============================================================
1749 from Gnumed.wxGladeWidgets import wxgVisualSoapPnl
1750
1752
1754
1755 wxgVisualSoapPnl.wxgVisualSoapPnl.__init__(self, *args, **kwargs)
1756
1757 # dummy episode to hold images
1758 self.default_episode_name = _('visual progress notes')
1759 #--------------------------------------------------------
1760 # external API
1761 #--------------------------------------------------------
1763 self._PRW_template.SetText(value = u'', data = None)
1764 self._LCTRL_visual_soaps.set_columns([_('Sketches')])
1765 self._LCTRL_visual_soaps.set_string_items()
1766
1767 self.show_image_and_metadata()
1768 #--------------------------------------------------------
1770
1771 self.clear()
1772
1773 if patient is None:
1774 patient = gmPerson.gmCurrentPatient()
1775
1776 if not patient.connected:
1777 return
1778
1779 emr = patient.get_emr()
1780 if encounter is None:
1781 encounter = emr.active_encounter
1782
1783 folder = patient.get_document_folder()
1784 soap_docs = folder.get_documents (
1785 doc_type = visual_progress_note_document_type,
1786 encounter = encounter['pk_encounter']
1787 )
1788
1789 if len(soap_docs) == 0:
1790 self._BTN_delete.Enable(False)
1791 return
1792
1793 self._LCTRL_visual_soaps.set_string_items ([
1794 u'%s%s%s' % (
1795 gmTools.coalesce(sd['comment'], u'', u'%s\n'),
1796 gmTools.coalesce(sd['ext_ref'], u'', u'%s\n'),
1797 sd['episode']
1798 ) for sd in soap_docs
1799 ])
1800 self._LCTRL_visual_soaps.set_data(soap_docs)
1801
1802 self._BTN_delete.Enable(True)
1803 #--------------------------------------------------------
1805
1806 if doc is None:
1807 self._IMG_soap.SetBitmap(wx.NullBitmap)
1808 self._PRW_episode.SetText()
1809 #self._PRW_comment.SetText(value = u'', data = None)
1810 self._PRW_comment.SetValue(u'')
1811 return
1812
1813 parts = doc.parts
1814 if len(parts) == 0:
1815 gmDispatcher.send(signal = u'statustext', msg = _('No images in visual progress note.'))
1816 return
1817
1818 fname = parts[0].export_to_file()
1819 if fname is None:
1820 gmDispatcher.send(signal = u'statustext', msg = _('Cannot export visual progress note to file.'))
1821 return
1822
1823 img_data = None
1824 rescaled_width = 300
1825 try:
1826 img_data = wx.Image(fname, wx.BITMAP_TYPE_ANY)
1827 current_width = img_data.GetWidth()
1828 current_height = img_data.GetHeight()
1829 rescaled_height = (rescaled_width * current_height) / current_width
1830 img_data.Rescale(rescaled_width, rescaled_height, quality = wx.IMAGE_QUALITY_HIGH) # w, h
1831 bmp_data = wx.BitmapFromImage(img_data)
1832 except:
1833 _log.exception('cannot load visual progress note from [%s]', fname)
1834 gmDispatcher.send(signal = u'statustext', msg = _('Cannot load visual progress note from [%s].') % fname)
1835 del img_data
1836 return
1837
1838 del img_data
1839 self._IMG_soap.SetBitmap(bmp_data)
1840
1841 self._PRW_episode.SetText(value = doc['episode'], data = doc['pk_episode'])
1842 if doc['comment'] is not None:
1843 self._PRW_comment.SetValue(doc['comment'].strip())
1844 #--------------------------------------------------------
1845 # event handlers
1846 #--------------------------------------------------------
1848
1849 doc = self._LCTRL_visual_soaps.get_selected_item_data(only_one = True)
1850 self.show_image_and_metadata(doc = doc)
1851 if doc is None:
1852 return
1853
1854 self._BTN_delete.Enable(True)
1855 #--------------------------------------------------------
1858 #--------------------------------------------------------
1860
1861 doc = self._LCTRL_visual_soaps.get_selected_item_data(only_one = True)
1862 if doc is None:
1863 self.show_image_and_metadata()
1864 return
1865
1866 parts = doc.parts
1867 if len(parts) == 0:
1868 gmDispatcher.send(signal = u'statustext', msg = _('No images in visual progress note.'))
1869 return
1870
1871 edit_visual_progress_note(doc_part = parts[0], discard_unmodified = True)
1872 self.show_image_and_metadata(doc = doc)
1873
1874 self._BTN_delete.Enable(True)
1875 #--------------------------------------------------------
1905 #--------------------------------------------------------
1968 #--------------------------------------------------------
1986 #============================================================
1987 # main
1988 #------------------------------------------------------------
1989 if __name__ == '__main__':
1990
1991 if len(sys.argv) < 2:
1992 sys.exit()
1993
1994 if sys.argv[1] != 'test':
1995 sys.exit()
1996
1997 gmI18N.activate_locale()
1998 gmI18N.install_domain(domain = 'gnumed')
1999
2000 #----------------------------------------
2002 pat = gmPersonSearch.ask_for_patient()
2003 gmPatSearchWidgets.set_active_patient(patient = pat)
2004 app = wx.PyWidgetTester(size = (200, 200))
2005 sels = select_narrative_from_episodes()
2006 print "selected:"
2007 for sel in sels:
2008 print sel
2009 #----------------------------------------
2011 pat = gmPersonSearch.ask_for_patient()
2012 application = wx.PyWidgetTester(size=(800,500))
2013 soap_input = cSoapNoteExpandoEditAreaPnl(application.frame, -1)
2014 application.frame.Show(True)
2015 application.MainLoop()
2016 #----------------------------------------
2018 patient = gmPersonSearch.ask_for_patient()
2019 if patient is None:
2020 print "No patient. Exiting gracefully..."
2021 return
2022 gmPatSearchWidgets.set_active_patient(patient=patient)
2023
2024 application = wx.PyWidgetTester(size=(800,500))
2025 soap_input = cSoapPluginPnl(application.frame, -1)
2026 application.frame.Show(True)
2027 soap_input._schedule_data_reget()
2028 application.MainLoop()
2029 #----------------------------------------
2030 #test_select_narrative_from_episodes()
2031 test_cSoapNoteExpandoEditAreaPnl()
2032 #test_cSoapPluginPnl()
2033
2034 #============================================================
2035
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Thu Sep 9 04:06:36 2010 | http://epydoc.sourceforge.net |