1 """GNUmed EMR structure editors
2
3 This module contains widgets to create and edit EMR structural
4 elements (issues, enconters, episodes).
5
6 This is based on initial work and ideas by Syan <kittylitter@swiftdsl.com.au>
7 and Karsten <Karsten.Hilbert@gmx.net>.
8 """
9
10 __author__ = "cfmoro1976@yahoo.es, karsten.hilbert@gmx.net"
11 __license__ = "GPL v2 or later"
12
13
14 import sys
15 import time
16 import logging
17 import datetime as pydt
18
19
20
21 import wx
22
23
24
25 if __name__ == '__main__':
26 sys.path.insert(0, '../../')
27 from Gnumed.pycommon import gmI18N
28 from Gnumed.pycommon import gmExceptions
29 from Gnumed.pycommon import gmCfg
30 from Gnumed.pycommon import gmDateTime
31 from Gnumed.pycommon import gmTools
32 from Gnumed.pycommon import gmDispatcher
33 from Gnumed.pycommon import gmMatchProvider
34
35 from Gnumed.business import gmEMRStructItems
36 from Gnumed.business import gmPraxis
37 from Gnumed.business import gmPerson
38
39 from Gnumed.wxpython import gmPhraseWheel
40 from Gnumed.wxpython import gmGuiHelpers
41 from Gnumed.wxpython import gmListWidgets
42 from Gnumed.wxpython import gmEditArea
43 from Gnumed.wxpython import gmOrganizationWidgets
44
45
46 _log = logging.getLogger('gm.ui')
47
48
49
50
52 """Spin time in seconds."""
53 if time2spin == 0:
54 return
55 sleep_time = 0.1
56 total_rounds = int(time2spin / sleep_time)
57 if total_rounds < 1:
58 return
59 rounds = 0
60 while rounds < total_rounds:
61 wx.Yield()
62 time.sleep(sleep_time)
63 rounds += 1
64
65
66
77
78 def delete(procedure=None):
79 if gmEMRStructItems.delete_performed_procedure(procedure = procedure['pk_procedure']):
80 return True
81
82 gmDispatcher.send (
83 signal = u'statustext',
84 msg = _('Cannot delete performed procedure.'),
85 beep = True
86 )
87 return False
88
89 def refresh(lctrl):
90 procs = emr.get_performed_procedures()
91
92 items = [
93 [
94 u'%s%s' % (
95 p['clin_when'].strftime('%Y-%m-%d'),
96 gmTools.bool2subst (
97 p['is_ongoing'],
98 _(' (ongoing)'),
99 gmTools.coalesce (
100 initial = p['clin_end'],
101 instead = u'',
102 template_initial = u' - %s',
103 function_initial = ('strftime', u'%Y-%m-%d')
104 )
105 )
106 ),
107 u'%s @ %s' % (p['unit'], p['organization']),
108 p['episode'],
109 p['performed_procedure']
110 ] for p in procs
111 ]
112 lctrl.set_string_items(items = items)
113 lctrl.set_data(data = procs)
114
115 gmListWidgets.get_choices_from_list (
116 parent = parent,
117 msg = _('\nSelect the procedure you want to edit !\n'),
118 caption = _('Editing performed procedures ...'),
119 columns = [_('When'), _('Where'), _('Episode'), _('Procedure')],
120 single_selection = True,
121 edit_callback = edit,
122 new_callback = edit,
123 delete_callback = delete,
124 refresh_callback = refresh
125 )
126
138
139 from Gnumed.wxGladeWidgets import wxgProcedureEAPnl
140
141 -class cProcedureEAPnl(wxgProcedureEAPnl.wxgProcedureEAPnl, gmEditArea.cGenericEditAreaMixin):
142
151
172
190
192 loc = self._PRW_location.GetData()
193 if loc is None:
194 self._PRW_hospital_stay.Enable(True)
195 self._PRW_hospital_stay.display_as_disabled(False)
196 self._PRW_episode.Enable(False)
197 self._PRW_episode.display_as_disabled(True)
198 else:
199 self._PRW_hospital_stay.SetText()
200 self._PRW_hospital_stay.Enable(False)
201 self._PRW_hospital_stay.display_as_disabled(True)
202 self._PRW_episode.Enable(True)
203 self._PRW_episode.display_as_disabled(False)
204
216
239
240
241
299
331
351
353 self._DPRW_date.SetText()
354 self._DPRW_end.SetText()
355 self._CHBOX_ongoing.SetValue(False)
356 self._CHBOX_ongoing.Enable(True)
357 self._PRW_hospital_stay.SetText()
358 self._LBL_hospital_details.SetLabel(u'')
359 self._PRW_location.SetText()
360 self._PRW_episode.SetText()
361 self._PRW_procedure.SetText()
362 self._PRW_codes.SetText()
363
364 self._PRW_procedure.SetFocus()
365
408
436
437
438
443
447
463
464
465
466
468
469 pat = gmPerson.gmCurrentPatient()
470 emr = pat.get_emr()
471
472 if parent is None:
473 parent = wx.GetApp().GetTopWindow()
474
475 def get_tooltip(stay=None):
476 if stay is None:
477 return None
478 return stay.format (
479 include_procedures = True,
480 include_docs = True
481 )
482
483 def edit(stay=None):
484 return edit_hospital_stay(parent = parent, hospital_stay = stay)
485
486 def delete(stay=None):
487 if gmEMRStructItems.delete_hospital_stay(stay = stay['pk_hospital_stay']):
488 return True
489 gmDispatcher.send (
490 signal = u'statustext',
491 msg = _('Cannot delete hospitalization.'),
492 beep = True
493 )
494 return False
495
496 def refresh(lctrl):
497 stays = emr.get_hospital_stays()
498 items = [
499 [
500 s['admission'].strftime('%Y-%m-%d'),
501 gmTools.coalesce(s['discharge'], u'', function_initial = ('strftime', '%Y-%m-%d')),
502 s['episode'],
503 u'%s @ %s' % (s['ward'], s['hospital'])
504 ] for s in stays
505 ]
506 lctrl.set_string_items(items = items)
507 lctrl.set_data(data = stays)
508
509 gmListWidgets.get_choices_from_list (
510 parent = parent,
511 msg = _("The patient's hospitalizations:\n"),
512 caption = _('Editing hospitalizations ...'),
513 columns = [_('Admission'), _('Discharge'), _('Reason'), _('Hospital')],
514 single_selection = True,
515 edit_callback = edit,
516 new_callback = edit,
517 delete_callback = delete,
518 refresh_callback = refresh,
519 list_tooltip_callback = get_tooltip
520 )
521
522
534
535
537 """Phrasewheel to allow selection of a hospitalization."""
539
540 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
541
542 query = u"""
543 SELECT data, list_label, field_label FROM (
544 SELECT DISTINCT ON (data) * FROM ((
545
546 -- already-used org_units
547 SELECT
548 pk_org_unit
549 AS data,
550 ward || ' @ ' || hospital
551 AS list_label,
552 ward || ' @ ' || hospital
553 AS field_label,
554 1
555 AS rank
556 FROM
557 clin.v_hospital_stays
558 WHERE
559 ward %(fragment_condition)s
560 OR
561 hospital %(fragment_condition)s
562
563 ) UNION ALL (
564 -- wards
565 SELECT
566 pk_org_unit
567 AS data,
568 unit || ' (' || l10n_unit_category || ') @ ' || organization
569 AS list_label,
570 unit || ' @ ' || organization
571 AS field_label,
572 2
573 AS rank
574 FROM
575 dem.v_org_units
576 WHERE
577 unit_category = 'Ward'
578 AND
579 unit %(fragment_condition)s
580 AND
581 NOT EXISTS (
582 SELECT 1 FROM clin.v_hospital_stays WHERE clin.v_hospital_stays.pk_org_unit = dem.v_org_units.pk_org_unit
583 )
584
585 ) UNION ALL (
586 -- hospital units
587 SELECT
588 pk_org_unit
589 AS data,
590 unit || coalesce(' (' || l10n_unit_category || ')', '') || ' @ ' || organization || ' (' || l10n_organization_category || ')'
591 AS list_label,
592 unit || ' @ ' || organization
593 AS field_label,
594 3
595 AS rank
596 FROM
597 dem.v_org_units
598 WHERE
599 unit_category <> 'Ward'
600 AND
601 organization_category = 'Hospital'
602 AND
603 unit %(fragment_condition)s
604 AND
605 NOT EXISTS (
606 SELECT 1 FROM clin.v_hospital_stays WHERE clin.v_hospital_stays.pk_org_unit = dem.v_org_units.pk_org_unit
607 )
608
609 ) UNION ALL (
610 -- any other units
611 SELECT
612 pk_org_unit
613 AS data,
614 unit || coalesce(' (' || l10n_unit_category || ')', '') || ' @ ' || organization || ' (' || l10n_organization_category || ')'
615 AS list_label,
616 unit || ' @ ' || organization
617 AS field_label,
618 3
619 AS rank
620 FROM
621 dem.v_org_units
622 WHERE
623 unit_category <> 'Ward'
624 AND
625 organization_category <> 'Hospital'
626 AND
627 unit %(fragment_condition)s
628 AND
629 NOT EXISTS (
630 SELECT 1 FROM clin.v_hospital_stays WHERE clin.v_hospital_stays.pk_org_unit = dem.v_org_units.pk_org_unit
631 )
632 )) AS all_matches
633 ORDER BY data, rank
634 ) AS distinct_matches
635 ORDER BY rank, list_label
636 LIMIT 50
637 """
638
639 mp = gmMatchProvider.cMatchProvider_SQL2(queries = [query])
640 mp.setThresholds(2, 4, 6)
641 self.matcher = mp
642 self.selection_only = True
643
644
646 """Phrasewheel to allow selection of a hospital-type org_unit."""
648
649 gmPhraseWheel.cPhraseWheel.__init__ (self, *args, **kwargs)
650
651 ctxt = {'ctxt_pat': {'where_part': u'pk_patient = %(pat)s and', 'placeholder': u'pat'}}
652
653 mp = gmMatchProvider.cMatchProvider_SQL2 (
654 queries = [
655 u"""
656 SELECT
657 pk_hospital_stay,
658 descr
659 FROM (
660 SELECT DISTINCT ON (pk_hospital_stay)
661 pk_hospital_stay,
662 descr
663 FROM
664 (SELECT
665 pk_hospital_stay,
666 (
667 to_char(admission, 'YYYY-Mon-DD')
668 || ' (' || ward || ' @ ' || hospital || '):'
669 || episode
670 || coalesce((' (' || health_issue || ')'), '')
671 ) AS descr
672 FROM
673 clin.v_hospital_stays
674 WHERE
675 %(ctxt_pat)s
676
677 hospital %(fragment_condition)s
678 OR
679 ward %(fragment_condition)s
680 OR
681 episode %(fragment_condition)s
682 OR
683 health_issue %(fragment_condition)s
684 ) AS the_stays
685 ) AS distinct_stays
686 ORDER BY descr
687 LIMIT 25
688 """ ],
689 context = ctxt
690 )
691 mp.setThresholds(3, 4, 6)
692 mp.set_context('pat', gmPerson.gmCurrentPatient().ID)
693
694 self.matcher = mp
695 self.selection_only = True
696
697
698 from Gnumed.wxGladeWidgets import wxgHospitalStayEditAreaPnl
699
700 -class cHospitalStayEditAreaPnl(wxgHospitalStayEditAreaPnl.wxgHospitalStayEditAreaPnl, gmEditArea.cGenericEditAreaMixin):
701
705
706
707
709
710 valid = True
711
712 if self._PRW_episode.GetValue().strip() == u'':
713 valid = False
714 self._PRW_episode.display_as_valid(False)
715 gmDispatcher.send(signal = 'statustext', msg = _('Must select an episode or enter a name for a new one. Cannot save hospitalization.'), beep = True)
716 self._PRW_episode.SetFocus()
717
718 if not self._PRW_admission.is_valid_timestamp(allow_empty = False):
719 valid = False
720 gmDispatcher.send(signal = 'statustext', msg = _('Missing admission data. Cannot save hospitalization.'), beep = True)
721 self._PRW_admission.SetFocus()
722
723 if self._PRW_discharge.is_valid_timestamp(allow_empty = True):
724 if self._PRW_discharge.date is not None:
725 adm = self._PRW_admission.date
726 discharge = self._PRW_discharge.date
727
728 discharge = discharge.replace (
729 hour = adm.hour,
730 minute = adm.minute,
731 second = adm.second,
732 microsecond = adm.microsecond
733 )
734 if adm is not None:
735 if discharge == adm:
736 self._PRW_discharge.SetData(discharge + pydt.timedelta(seconds = 1))
737 elif not self._PRW_discharge.date > self._PRW_admission.date:
738 valid = False
739 self._PRW_discharge.display_as_valid(False)
740 gmDispatcher.send(signal = 'statustext', msg = _('Discharge date must be empty or later than admission. Cannot save hospitalization.'), beep = True)
741 self._PRW_discharge.SetFocus()
742
743 if self._PRW_hospital.GetData() is None:
744 self._PRW_hospital.display_as_valid(False)
745 self.status_message = _('Must select a hospital. Cannot save hospitalization.')
746 self._PRW_hospital.SetFocus()
747 else:
748 self._PRW_hospital.display_as_valid(True)
749
750 return (valid is True)
751
765
767
768 self.data['pk_episode'] = self._PRW_episode.GetData(can_create = True)
769 self.data['pk_org_unit'] = self._PRW_hospital.GetData()
770 self.data['admission'] = self._PRW_admission.GetData()
771 self.data['discharge'] = self._PRW_discharge.GetData()
772 self.data['comment'] = self._TCTRL_comment.GetValue()
773 self.data.save_payload()
774
775 return True
776
784
796
798 print "this was not expected to be used in this edit area"
799
800
801
802
812
882
884 """Prepare changing health issue for an episode.
885
886 Checks for two-open-episodes conflict. When this
887 function succeeds, the pk_health_issue has been set
888 on the episode instance and the episode should - for
889 all practical purposes - be ready for save_payload().
890 """
891
892 if not episode['episode_open']:
893 episode['pk_health_issue'] = target_issue['pk_health_issue']
894 if save_to_backend:
895 episode.save_payload()
896 return True
897
898
899 if target_issue is None:
900 episode['pk_health_issue'] = None
901 if save_to_backend:
902 episode.save_payload()
903 return True
904
905
906 db_cfg = gmCfg.cCfgSQL()
907 epi_ttl = int(db_cfg.get2 (
908 option = u'episode.ttl',
909 workplace = gmPraxis.gmCurrentPraxisBranch().active_workplace,
910 bias = 'user',
911 default = 60
912 ))
913 if target_issue.close_expired_episode(ttl=epi_ttl) is True:
914 gmDispatcher.send(signal='statustext', msg=_('Closed episodes older than %s days on health issue [%s]') % (epi_ttl, target_issue['description']))
915 existing_epi = target_issue.get_open_episode()
916
917
918 if existing_epi is None:
919 episode['pk_health_issue'] = target_issue['pk_health_issue']
920 if save_to_backend:
921 episode.save_payload()
922 return True
923
924
925 if existing_epi['pk_episode'] == episode['pk_episode']:
926 episode['pk_health_issue'] = target_issue['pk_health_issue']
927 if save_to_backend:
928 episode.save_payload()
929 return True
930
931
932 move_range = episode.get_access_range()
933 exist_range = existing_epi.get_access_range()
934 question = _(
935 'You want to associate the running episode:\n\n'
936 ' "%(new_epi_name)s" (%(new_epi_start)s - %(new_epi_end)s)\n\n'
937 'with the health issue:\n\n'
938 ' "%(issue_name)s"\n\n'
939 'There already is another episode running\n'
940 'for this health issue:\n\n'
941 ' "%(old_epi_name)s" (%(old_epi_start)s - %(old_epi_end)s)\n\n'
942 'However, there can only be one running\n'
943 'episode per health issue.\n\n'
944 'Which episode do you want to close ?'
945 ) % {
946 'new_epi_name': episode['description'],
947 'new_epi_start': move_range[0].strftime('%m/%y'),
948 'new_epi_end': move_range[1].strftime('%m/%y'),
949 'issue_name': target_issue['description'],
950 'old_epi_name': existing_epi['description'],
951 'old_epi_start': exist_range[0].strftime('%m/%y'),
952 'old_epi_end': exist_range[1].strftime('%m/%y')
953 }
954 dlg = gmGuiHelpers.c3ButtonQuestionDlg (
955 parent = None,
956 id = -1,
957 caption = _('Resolving two-running-episodes conflict'),
958 question = question,
959 button_defs = [
960 {'label': _('old episode'), 'default': True, 'tooltip': _('close existing episode "%s"') % existing_epi['description']},
961 {'label': _('new episode'), 'default': False, 'tooltip': _('close moving (new) episode "%s"') % episode['description']}
962 ]
963 )
964 decision = dlg.ShowModal()
965
966 if decision == wx.ID_CANCEL:
967
968 return False
969
970 elif decision == wx.ID_YES:
971
972 existing_epi['episode_open'] = False
973 existing_epi.save_payload()
974
975 elif decision == wx.ID_NO:
976
977 episode['episode_open'] = False
978
979 else:
980 raise ValueError('invalid result from c3ButtonQuestionDlg: [%s]' % decision)
981
982 episode['pk_health_issue'] = target_issue['pk_health_issue']
983 if save_to_backend:
984 episode.save_payload()
985 return True
986
1010
1012 """Let user select an episode *description*.
1013
1014 The user can select an episode description from the previously
1015 used descriptions across all episodes across all patients.
1016
1017 Selection is done with a phrasewheel so the user can
1018 type the episode name and matches will be shown. Typing
1019 "*" will show the entire list of episodes.
1020
1021 If the user types a description not existing yet a
1022 new episode description will be returned.
1023 """
1025
1026 mp = gmMatchProvider.cMatchProvider_SQL2 (
1027 queries = [
1028 u"""
1029 SELECT DISTINCT ON (description)
1030 description
1031 AS data,
1032 description
1033 AS field_label,
1034 description || ' ('
1035 || CASE
1036 WHEN is_open IS TRUE THEN _('ongoing')
1037 ELSE _('closed')
1038 END
1039 || ')'
1040 AS list_label
1041 FROM
1042 clin.episode
1043 WHERE
1044 description %(fragment_condition)s
1045 ORDER BY description
1046 LIMIT 30
1047 """
1048 ]
1049 )
1050 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
1051 self.matcher = mp
1052
1054 """Let user select an episode.
1055
1056 The user can select an episode from the existing episodes of a
1057 patient. Selection is done with a phrasewheel so the user
1058 can type the episode name and matches will be shown. Typing
1059 "*" will show the entire list of episodes. Closed episodes
1060 will be marked as such. If the user types an episode name not
1061 in the list of existing episodes a new episode can be created
1062 from it if the programmer activated that feature.
1063
1064 If keyword <patient_id> is set to None or left out the control
1065 will listen to patient change signals and therefore act on
1066 gmPerson.gmCurrentPatient() changes.
1067 """
1069
1070 ctxt = {'ctxt_pat': {'where_part': u'and pk_patient = %(pat)s', 'placeholder': u'pat'}}
1071
1072 mp = gmMatchProvider.cMatchProvider_SQL2 (
1073 queries = [
1074 u"""(
1075
1076 SELECT
1077 pk_episode
1078 as data,
1079 description
1080 as field_label,
1081 coalesce (
1082 description || ' - ' || health_issue,
1083 description
1084 ) as list_label,
1085 1 as rank
1086 from
1087 clin.v_pat_episodes
1088 where
1089 episode_open is true and
1090 description %(fragment_condition)s
1091 %(ctxt_pat)s
1092
1093 ) union all (
1094
1095 SELECT
1096 pk_episode
1097 as data,
1098 description
1099 as field_label,
1100 coalesce (
1101 description || _(' (closed)') || ' - ' || health_issue,
1102 description || _(' (closed)')
1103 ) as list_label,
1104 2 as rank
1105 from
1106 clin.v_pat_episodes
1107 where
1108 description %(fragment_condition)s and
1109 episode_open is false
1110 %(ctxt_pat)s
1111
1112 )
1113
1114 order by rank, list_label
1115 limit 30"""
1116 ],
1117 context = ctxt
1118 )
1119
1120 try:
1121 kwargs['patient_id']
1122 except KeyError:
1123 kwargs['patient_id'] = None
1124
1125 if kwargs['patient_id'] is None:
1126 self.use_current_patient = True
1127 self.__register_patient_change_signals()
1128 pat = gmPerson.gmCurrentPatient()
1129 if pat.connected:
1130 mp.set_context('pat', pat.ID)
1131 else:
1132 self.use_current_patient = False
1133 self.__patient_id = int(kwargs['patient_id'])
1134 mp.set_context('pat', self.__patient_id)
1135
1136 del kwargs['patient_id']
1137
1138 gmPhraseWheel.cPhraseWheel.__init__ (
1139 self,
1140 *args,
1141 **kwargs
1142 )
1143 self.matcher = mp
1144
1145
1146
1148 if self.use_current_patient:
1149 return False
1150 self.__patient_id = int(patient_id)
1151 self.set_context('pat', self.__patient_id)
1152 return True
1153
1154 - def GetData(self, can_create=False, as_instance=False, is_open=False):
1157
1159
1160 epi_name = self.GetValue().strip()
1161 if epi_name == u'':
1162 gmDispatcher.send(signal = u'statustext', msg = _('Cannot create episode without name.'), beep = True)
1163 _log.debug('cannot create episode without name')
1164 return
1165
1166 if self.use_current_patient:
1167 pat = gmPerson.gmCurrentPatient()
1168 else:
1169 pat = gmPerson.cPatient(aPK_obj = self.__patient_id)
1170
1171 emr = pat.get_emr()
1172 epi = emr.add_episode(episode_name = epi_name, is_open = self.__is_open_for_create_data)
1173 if epi is None:
1174 self.data = {}
1175 else:
1176 self.SetText (
1177 value = epi_name,
1178 data = epi['pk_episode']
1179 )
1180
1183
1184
1185
1189
1192
1194 if self.use_current_patient:
1195 patient = gmPerson.gmCurrentPatient()
1196 self.set_context('pat', patient.ID)
1197 return True
1198
1199 from Gnumed.wxGladeWidgets import wxgEpisodeEditAreaPnl
1200
1201 -class cEpisodeEditAreaPnl(gmEditArea.cGenericEditAreaMixin, wxgEpisodeEditAreaPnl.wxgEpisodeEditAreaPnl):
1202
1215
1216
1217
1219
1220 errors = False
1221
1222 if len(self._PRW_description.GetValue().strip()) == 0:
1223 errors = True
1224 self._PRW_description.display_as_valid(False)
1225 self._PRW_description.SetFocus()
1226 else:
1227 self._PRW_description.display_as_valid(True)
1228 self._PRW_description.Refresh()
1229
1230 return not errors
1231
1233
1234 pat = gmPerson.gmCurrentPatient()
1235 emr = pat.get_emr()
1236
1237 epi = emr.add_episode(episode_name = self._PRW_description.GetValue().strip())
1238 epi['summary'] = self._TCTRL_status.GetValue().strip()
1239 epi['episode_open'] = not self._CHBOX_closed.IsChecked()
1240 epi['diagnostic_certainty_classification'] = self._PRW_certainty.GetData()
1241
1242 issue_name = self._PRW_issue.GetValue().strip()
1243 if len(issue_name) != 0:
1244 epi['pk_health_issue'] = self._PRW_issue.GetData(can_create = True)
1245 issue = gmEMRStructItems.cHealthIssue(aPK_obj = epi['pk_health_issue'])
1246
1247 if not move_episode_to_issue(episode = epi, target_issue = issue, save_to_backend = False):
1248 gmDispatcher.send (
1249 signal = 'statustext',
1250 msg = _('Cannot attach episode [%s] to health issue [%s] because it already has a running episode.') % (
1251 epi['description'],
1252 issue['description']
1253 )
1254 )
1255 gmEMRStructItems.delete_episode(episode = epi)
1256 return False
1257
1258 epi.save()
1259
1260 epi.generic_codes = [ c['data'] for c in self._PRW_codes.GetData() ]
1261
1262 self.data = epi
1263 return True
1264
1266
1267 self.data['description'] = self._PRW_description.GetValue().strip()
1268 self.data['summary'] = self._TCTRL_status.GetValue().strip()
1269 self.data['episode_open'] = not self._CHBOX_closed.IsChecked()
1270 self.data['diagnostic_certainty_classification'] = self._PRW_certainty.GetData()
1271
1272 issue_name = self._PRW_issue.GetValue().strip()
1273 if len(issue_name) == 0:
1274 self.data['pk_health_issue'] = None
1275 else:
1276 self.data['pk_health_issue'] = self._PRW_issue.GetData(can_create = True)
1277 issue = gmEMRStructItems.cHealthIssue(aPK_obj = self.data['pk_health_issue'])
1278
1279 if not move_episode_to_issue(episode = self.data, target_issue = issue, save_to_backend = False):
1280 gmDispatcher.send (
1281 signal = 'statustext',
1282 msg = _('Cannot attach episode [%s] to health issue [%s] because it already has a running episode.') % (
1283 self.data['description'],
1284 issue['description']
1285 )
1286 )
1287 return False
1288
1289 self.data.save()
1290 self.data.generic_codes = [ c['data'] for c in self._PRW_codes.GetData() ]
1291
1292 return True
1293
1306
1325
1327 self._refresh_as_new()
1328
1329
1330
1342
1344
1345 if parent is None:
1346 parent = wx.GetApp().GetTopWindow()
1347
1348 def refresh(lctrl):
1349 issues = emr.get_health_issues()
1350 items = [
1351 [
1352 gmTools.bool2subst(i['is_confidential'], _('CONFIDENTIAL'), u'', u''),
1353 i['description'],
1354 gmTools.bool2subst(i['clinically_relevant'], _('relevant'), u'', u''),
1355 gmTools.bool2subst(i['is_active'], _('active'), u'', u''),
1356 gmTools.bool2subst(i['is_cause_of_death'], _('fatal'), u'', u'')
1357 ] for i in issues
1358 ]
1359 lctrl.set_string_items(items = items)
1360 lctrl.set_data(data = issues)
1361
1362 return gmListWidgets.get_choices_from_list (
1363 parent = parent,
1364 msg = _('\nSelect the health issues !\n'),
1365 caption = _('Showing health issues ...'),
1366 columns = [u'', _('Health issue'), u'', u'', u''],
1367 single_selection = False,
1368
1369
1370
1371 refresh_callback = refresh
1372 )
1373
1375
1376
1377
1379
1380 issues = kwargs['issues']
1381 del kwargs['issues']
1382
1383 gmListWidgets.cGenericListSelectorDlg.__init__(self, *args, **kwargs)
1384
1385 self.SetTitle(_('Select the health issues you are interested in ...'))
1386 self._LCTRL_items.set_columns([u'', _('Health Issue'), u'', u'', u''])
1387
1388 for issue in issues:
1389 if issue['is_confidential']:
1390 row_num = self._LCTRL_items.InsertStringItem(sys.maxint, label = _('confidential'))
1391 self._LCTRL_items.SetItemTextColour(row_num, col=wx.NamedColour('RED'))
1392 else:
1393 row_num = self._LCTRL_items.InsertStringItem(sys.maxint, label = u'')
1394
1395 self._LCTRL_items.SetStringItem(index = row_num, col = 1, label = issue['description'])
1396 if issue['clinically_relevant']:
1397 self._LCTRL_items.SetStringItem(index = row_num, col = 2, label = _('relevant'))
1398 if issue['is_active']:
1399 self._LCTRL_items.SetStringItem(index = row_num, col = 3, label = _('active'))
1400 if issue['is_cause_of_death']:
1401 self._LCTRL_items.SetStringItem(index = row_num, col = 4, label = _('fatal'))
1402
1403 self._LCTRL_items.set_column_widths()
1404 self._LCTRL_items.set_data(data = issues)
1405
1407 """Let the user select a health issue.
1408
1409 The user can select a health issue from the existing issues
1410 of a patient. Selection is done with a phrasewheel so the user
1411 can type the issue name and matches will be shown. Typing
1412 "*" will show the entire list of issues. Inactive issues
1413 will be marked as such. If the user types an issue name not
1414 in the list of existing issues a new issue can be created
1415 from it if the programmer activated that feature.
1416
1417 If keyword <patient_id> is set to None or left out the control
1418 will listen to patient change signals and therefore act on
1419 gmPerson.gmCurrentPatient() changes.
1420 """
1422
1423 ctxt = {'ctxt_pat': {'where_part': u'pk_patient=%(pat)s', 'placeholder': u'pat'}}
1424
1425 mp = gmMatchProvider.cMatchProvider_SQL2 (
1426
1427 queries = [
1428 u"""
1429 SELECT
1430 data,
1431 field_label,
1432 list_label
1433 FROM ((
1434 SELECT
1435 pk_health_issue AS data,
1436 description AS field_label,
1437 description AS list_label
1438 FROM clin.v_health_issues
1439 WHERE
1440 is_active IS true
1441 AND
1442 description %(fragment_condition)s
1443 AND
1444 %(ctxt_pat)s
1445
1446 ) UNION (
1447
1448 SELECT
1449 pk_health_issue AS data,
1450 description AS field_label,
1451 description || _(' (inactive)') AS list_label
1452 FROM clin.v_health_issues
1453 WHERE
1454 is_active IS false
1455 AND
1456 description %(fragment_condition)s
1457 AND
1458 %(ctxt_pat)s
1459 )) AS union_query
1460 ORDER BY
1461 list_label"""],
1462 context = ctxt
1463 )
1464
1465 try: kwargs['patient_id']
1466 except KeyError: kwargs['patient_id'] = None
1467
1468 if kwargs['patient_id'] is None:
1469 self.use_current_patient = True
1470 self.__register_patient_change_signals()
1471 pat = gmPerson.gmCurrentPatient()
1472 if pat.connected:
1473 mp.set_context('pat', pat.ID)
1474 else:
1475 self.use_current_patient = False
1476 self.__patient_id = int(kwargs['patient_id'])
1477 mp.set_context('pat', self.__patient_id)
1478
1479 del kwargs['patient_id']
1480
1481 gmPhraseWheel.cPhraseWheel.__init__ (
1482 self,
1483 *args,
1484 **kwargs
1485 )
1486 self.matcher = mp
1487
1488
1489
1491 if self.use_current_patient:
1492 return False
1493 self.__patient_id = int(patient_id)
1494 self.set_context('pat', self.__patient_id)
1495 return True
1496
1498 issue_name = self.GetValue().strip()
1499 if issue_name == u'':
1500 gmDispatcher.send(signal = u'statustext', msg = _('Cannot create health issue without name.'), beep = True)
1501 _log.debug('cannot create health issue without name')
1502 return
1503
1504 if self.use_current_patient:
1505 pat = gmPerson.gmCurrentPatient()
1506 else:
1507 pat = gmPerson.cPatient(aPK_obj = self.__patient_id)
1508
1509 emr = pat.get_emr()
1510 issue = emr.add_health_issue(issue_name = issue_name)
1511
1512 if issue is None:
1513 self.data = {}
1514 else:
1515 self.SetText (
1516 value = issue_name,
1517 data = issue['pk_health_issue']
1518 )
1519
1522
1523
1524
1528
1531
1533 if self.use_current_patient:
1534 patient = gmPerson.gmCurrentPatient()
1535 self.set_context('pat', patient.ID)
1536 return True
1537
1538 from Gnumed.wxGladeWidgets import wxgIssueSelectionDlg
1539
1562
1563 from Gnumed.wxGladeWidgets import wxgHealthIssueEditAreaPnl
1564
1565 -class cHealthIssueEditAreaPnl(gmEditArea.cGenericEditAreaMixin, wxgHealthIssueEditAreaPnl.wxgHealthIssueEditAreaPnl):
1566 """Panel encapsulating health issue edit area functionality."""
1567
1585
1587
1588
1589 mp = gmMatchProvider.cMatchProvider_SQL2 (
1590 queries = [u"SELECT DISTINCT ON (description) description, description FROM clin.health_issue WHERE description %(fragment_condition)s LIMIT 50"]
1591 )
1592 mp.setThresholds(1, 3, 5)
1593 self._PRW_condition.matcher = mp
1594
1595 mp = gmMatchProvider.cMatchProvider_SQL2 (
1596 queries = [u"""
1597 SELECT DISTINCT ON (grouping) grouping, grouping from (
1598
1599 SELECT rank, grouping from ((
1600
1601 SELECT
1602 grouping,
1603 1 as rank
1604 from
1605 clin.health_issue
1606 where
1607 grouping %%(fragment_condition)s
1608 and
1609 (SELECT True from clin.encounter where fk_patient = %s and pk = clin.health_issue.fk_encounter)
1610
1611 ) union (
1612
1613 SELECT
1614 grouping,
1615 2 as rank
1616 from
1617 clin.health_issue
1618 where
1619 grouping %%(fragment_condition)s
1620
1621 )) as union_result
1622
1623 order by rank
1624
1625 ) as order_result
1626
1627 limit 50""" % gmPerson.gmCurrentPatient().ID
1628 ]
1629 )
1630 mp.setThresholds(1, 3, 5)
1631 self._PRW_grouping.matcher = mp
1632
1633 self._PRW_age_noted.add_callback_on_lose_focus(self._on_leave_age_noted)
1634 self._PRW_year_noted.add_callback_on_lose_focus(self._on_leave_year_noted)
1635
1636
1637
1638
1639 self._PRW_year_noted.Enable(True)
1640
1641 self._PRW_codes.add_callback_on_lose_focus(self._on_leave_codes)
1642
1643
1644
1645
1664
1666 pat = gmPerson.gmCurrentPatient()
1667 emr = pat.get_emr()
1668
1669 issue = emr.add_health_issue(issue_name = self._PRW_condition.GetValue().strip())
1670
1671 side = u''
1672 if self._ChBOX_left.GetValue():
1673 side += u's'
1674 if self._ChBOX_right.GetValue():
1675 side += u'd'
1676 issue['laterality'] = side
1677
1678 issue['summary'] = self._TCTRL_status.GetValue().strip()
1679 issue['diagnostic_certainty_classification'] = self._PRW_certainty.GetData()
1680 issue['grouping'] = self._PRW_grouping.GetValue().strip()
1681 issue['is_active'] = self._ChBOX_active.GetValue()
1682 issue['clinically_relevant'] = self._ChBOX_relevant.GetValue()
1683 issue['is_confidential'] = self._ChBOX_confidential.GetValue()
1684 issue['is_cause_of_death'] = self._ChBOX_caused_death.GetValue()
1685
1686 age_noted = self._PRW_age_noted.GetData()
1687 if age_noted is not None:
1688 issue['age_noted'] = age_noted
1689
1690 issue.save()
1691
1692 issue.generic_codes = [ c['data'] for c in self._PRW_codes.GetData() ]
1693
1694 self.data = issue
1695 return True
1696
1698
1699 self.data['description'] = self._PRW_condition.GetValue().strip()
1700
1701 side = u''
1702 if self._ChBOX_left.GetValue():
1703 side += u's'
1704 if self._ChBOX_right.GetValue():
1705 side += u'd'
1706 self.data['laterality'] = side
1707
1708 self.data['summary'] = self._TCTRL_status.GetValue().strip()
1709 self.data['diagnostic_certainty_classification'] = self._PRW_certainty.GetData()
1710 self.data['grouping'] = self._PRW_grouping.GetValue().strip()
1711 self.data['is_active'] = bool(self._ChBOX_active.GetValue())
1712 self.data['clinically_relevant'] = bool(self._ChBOX_relevant.GetValue())
1713 self.data['is_confidential'] = bool(self._ChBOX_confidential.GetValue())
1714 self.data['is_cause_of_death'] = bool(self._ChBOX_caused_death.GetValue())
1715
1716 age_noted = self._PRW_age_noted.GetData()
1717 if age_noted is not None:
1718 self.data['age_noted'] = age_noted
1719
1720 self.data.save()
1721 self.data.generic_codes = [ c['data'] for c in self._PRW_codes.GetData() ]
1722
1723 return True
1724
1726 self._PRW_condition.SetText()
1727 self._ChBOX_left.SetValue(0)
1728 self._ChBOX_right.SetValue(0)
1729 self._PRW_codes.SetText()
1730 self._on_leave_codes()
1731 self._PRW_certainty.SetText()
1732 self._PRW_grouping.SetText()
1733 self._TCTRL_status.SetValue(u'')
1734 self._PRW_age_noted.SetText()
1735 self._PRW_year_noted.SetText()
1736 self._ChBOX_active.SetValue(1)
1737 self._ChBOX_relevant.SetValue(1)
1738 self._ChBOX_confidential.SetValue(0)
1739 self._ChBOX_caused_death.SetValue(0)
1740
1741 self._PRW_condition.SetFocus()
1742 return True
1743
1782
1784 return self._refresh_as_new()
1785
1786
1787
1789 if not self._PRW_codes.IsModified():
1790 return True
1791
1792 self._TCTRL_code_details.SetValue(u'- ' + u'\n- '.join([ c['list_label'] for c in self._PRW_codes.GetData() ]))
1793
1795
1796 if not self._PRW_age_noted.IsModified():
1797 return True
1798
1799 age_str = self._PRW_age_noted.GetValue().strip()
1800
1801 if age_str == u'':
1802 return True
1803
1804 issue_age = gmDateTime.str2interval(str_interval = age_str)
1805
1806 if issue_age is None:
1807 self.status_message = _('Cannot parse [%s] into valid interval.') % age_str
1808 self._PRW_age_noted.display_as_valid(False)
1809 return True
1810
1811 pat = gmPerson.gmCurrentPatient()
1812 if pat['dob'] is not None:
1813 max_issue_age = pydt.datetime.now(tz=pat['dob'].tzinfo) - pat['dob']
1814 if issue_age >= max_issue_age:
1815 self.status_message = _('Health issue cannot have been noted at age %s. Patient is only %s old.') % (issue_age, pat.get_medical_age())
1816 self._PRW_age_noted.display_as_valid(False)
1817 return True
1818
1819 self._PRW_age_noted.display_as_valid(True)
1820 self._PRW_age_noted.SetText(value = age_str, data = issue_age)
1821
1822 if pat['dob'] is not None:
1823 fts = gmDateTime.cFuzzyTimestamp (
1824 timestamp = pat['dob'] + issue_age,
1825 accuracy = gmDateTime.acc_months
1826 )
1827 self._PRW_year_noted.SetText(value = str(fts), data = fts)
1828
1829 return True
1830
1832
1833 if not self._PRW_year_noted.IsModified():
1834 return True
1835
1836 year_noted = self._PRW_year_noted.GetData()
1837
1838 if year_noted is None:
1839 if self._PRW_year_noted.GetValue().strip() == u'':
1840 self._PRW_year_noted.display_as_valid(True)
1841 return True
1842 self._PRW_year_noted.display_as_valid(False)
1843 return True
1844
1845 year_noted = year_noted.get_pydt()
1846
1847 if year_noted >= pydt.datetime.now(tz = year_noted.tzinfo):
1848 self.status_message = _('Condition diagnosed in the future.')
1849 self._PRW_year_noted.display_as_valid(False)
1850 return True
1851
1852 self._PRW_year_noted.display_as_valid(True)
1853
1854 pat = gmPerson.gmCurrentPatient()
1855 if pat['dob'] is not None:
1856 issue_age = year_noted - pat['dob']
1857 age_str = gmDateTime.format_interval_medically(interval = issue_age)
1858 self._PRW_age_noted.SetText(age_str, issue_age, True)
1859
1860 return True
1861
1863 wx.CallAfter(self._PRW_year_noted.SetText, u'', None, True)
1864 return True
1865
1867 wx.CallAfter(self._PRW_age_noted.SetText, u'', None, True)
1868 return True
1869
1870
1871
1873
1875
1876 gmPhraseWheel.cPhraseWheel.__init__(self, *args, **kwargs)
1877
1878 self.selection_only = False
1879
1880 mp = gmMatchProvider.cMatchProvider_FixedList (
1881 aSeq = [
1882 {'data': u'A', 'list_label': gmEMRStructItems.diagnostic_certainty_classification2str(u'A'), 'field_label': gmEMRStructItems.diagnostic_certainty_classification2str(u'A'), 'weight': 1},
1883 {'data': u'B', 'list_label': gmEMRStructItems.diagnostic_certainty_classification2str(u'B'), 'field_label': gmEMRStructItems.diagnostic_certainty_classification2str(u'B'), 'weight': 1},
1884 {'data': u'C', 'list_label': gmEMRStructItems.diagnostic_certainty_classification2str(u'C'), 'field_label': gmEMRStructItems.diagnostic_certainty_classification2str(u'C'), 'weight': 1},
1885 {'data': u'D', 'list_label': gmEMRStructItems.diagnostic_certainty_classification2str(u'D'), 'field_label': gmEMRStructItems.diagnostic_certainty_classification2str(u'D'), 'weight': 1}
1886 ]
1887 )
1888 mp.setThresholds(1, 2, 4)
1889 self.matcher = mp
1890
1891 self.SetToolTipString(_(
1892 "The diagnostic classification or grading of this assessment.\n"
1893 "\n"
1894 "This documents how certain one is about this being a true diagnosis."
1895 ))
1896
1897
1898
1899
1900 if __name__ == '__main__':
1901
1902 from Gnumed.business import gmPersonSearch
1903 from Gnumed.wxpython import gmPatSearchWidgets
1904
1905
1907 """
1908 Test application for testing EMR struct widgets
1909 """
1910
1912 """
1913 Create test application UI
1914 """
1915 frame = wx.Frame (
1916 None,
1917 -4,
1918 'Testing EMR struct widgets',
1919 size=wx.Size(600, 400),
1920 style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE
1921 )
1922 filemenu= wx.Menu()
1923 filemenu.AppendSeparator()
1924 filemenu.Append(ID_EXIT,"E&xit"," Terminate test application")
1925
1926
1927 menuBar = wx.MenuBar()
1928 menuBar.Append(filemenu,"&File")
1929
1930 frame.SetMenuBar(menuBar)
1931
1932 txt = wx.StaticText( frame, -1, _("Select desired test option from the 'File' menu"),
1933 wx.DefaultPosition, wx.DefaultSize, 0 )
1934
1935
1936 wx.EVT_MENU(frame, ID_EXIT, self.OnCloseWindow)
1937
1938
1939 self.__pat = gmPerson.gmCurrentPatient()
1940
1941 frame.Show(1)
1942 return 1
1943
1945 """
1946 Close test aplication
1947 """
1948 self.ExitMainLoop ()
1949
1950
1958
1964
1966 app = wx.PyWidgetTester(size = (400, 40))
1967 app.SetWidget(cHospitalStayPhraseWheel, id=-1, size=(180,20), pos=(10,20))
1968 app.MainLoop()
1969
1971 app = wx.PyWidgetTester(size = (400, 40))
1972 app.SetWidget(cEpisodeSelectionPhraseWheel, id=-1, size=(180,20), pos=(10,20))
1973
1974 app.MainLoop()
1975
1977 app = wx.PyWidgetTester(size = (200, 300))
1978 edit_health_issue(parent=app.frame, issue=None)
1979
1981 app = wx.PyWidgetTester(size = (200, 300))
1982 app.SetWidget(cHealthIssueEditAreaPnl, id=-1, size = (400,400))
1983 app.MainLoop()
1984
1986 app = wx.PyWidgetTester(size = (200, 300))
1987 edit_procedure(parent=app.frame)
1988
1989
1990 if (len(sys.argv) > 1) and (sys.argv[1] == 'test'):
1991
1992 gmI18N.activate_locale()
1993 gmI18N.install_domain()
1994 gmDateTime.init()
1995
1996
1997 pat = gmPersonSearch.ask_for_patient()
1998 if pat is None:
1999 print "No patient. Exiting gracefully..."
2000 sys.exit(0)
2001 gmPatSearchWidgets.set_active_patient(patient=pat)
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017 test_edit_procedure()
2018
2019
2020