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