Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/fast.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

12 CamCOPS is free software: you can redistribute it and/or modify 

13 it under the terms of the GNU General Public License as published by 

14 the Free Software Foundation, either version 3 of the License, or 

15 (at your option) any later version. 

16 

17 CamCOPS is distributed in the hope that it will be useful, 

18 but WITHOUT ANY WARRANTY; without even the implied warranty of 

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

22 You should have received a copy of the GNU General Public License 

23 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

24 

25=============================================================================== 

26 

27""" 

28 

29from typing import Any, Dict, List, Tuple, Type 

30 

31from cardinal_pythonlib.stringfunc import strseq 

32from sqlalchemy.ext.declarative import DeclarativeMeta 

33from sqlalchemy.sql.sqltypes import Boolean, Integer 

34 

35from camcops_server.cc_modules.cc_constants import CssClass 

36from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

37from camcops_server.cc_modules.cc_db import add_multiple_columns 

38from camcops_server.cc_modules.cc_html import answer, get_yes_no, tr, tr_qa 

39from camcops_server.cc_modules.cc_request import CamcopsRequest 

40from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

41from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

42from camcops_server.cc_modules.cc_task import ( 

43 get_from_dict, 

44 Task, 

45 TaskHasPatientMixin, 

46) 

47from camcops_server.cc_modules.cc_text import SS 

48from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

49 

50 

51# ============================================================================= 

52# FAST 

53# ============================================================================= 

54 

55class FastMetaclass(DeclarativeMeta): 

56 # noinspection PyInitNewSignature 

57 def __init__(cls: Type['Fast'], 

58 name: str, 

59 bases: Tuple[Type, ...], 

60 classdict: Dict[str, Any]) -> None: 

61 add_multiple_columns( 

62 cls, "q", 1, cls.NQUESTIONS, 

63 minimum=0, maximum=4, 

64 comment_fmt="Q{n}. {s} (0-4, higher worse)", 

65 comment_strings=[ 

66 "M>8, F>6 drinks", "unable to remember", 

67 "failed to do what was expected", "others concerned" 

68 ] 

69 ) 

70 super().__init__(name, bases, classdict) 

71 

72 

73class Fast(TaskHasPatientMixin, Task, 

74 metaclass=FastMetaclass): 

75 """ 

76 Server implementation of the FAST task. 

77 """ 

78 __tablename__ = "fast" 

79 shortname = "FAST" 

80 

81 NQUESTIONS = 4 

82 TASK_FIELDS = strseq("q", 1, NQUESTIONS) 

83 MAX_SCORE = 16 

84 

85 @staticmethod 

86 def longname(req: "CamcopsRequest") -> str: 

87 _ = req.gettext 

88 return _("Fast Alcohol Screening Test") 

89 

90 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: 

91 return [TrackerInfo( 

92 value=self.total_score(), 

93 plot_label="FAST total score", 

94 axis_label=f"Total score (out of {self.MAX_SCORE})", 

95 axis_min=-0.5, 

96 axis_max=self.MAX_SCORE + 0.5 

97 )] 

98 

99 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: 

100 if not self.is_complete(): 

101 return CTV_INCOMPLETE 

102 classification = "positive" if self.is_positive() else "negative" 

103 return [CtvInfo(content=( 

104 f"FAST total score {self.total_score()}/{self.MAX_SCORE} " 

105 f"({classification})" 

106 ))] 

107 

108 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: 

109 return self.standard_task_summary_fields() + [ 

110 SummaryElement(name="total", 

111 coltype=Integer(), 

112 value=self.total_score(), 

113 comment=f"Total score (/{self.MAX_SCORE})"), 

114 SummaryElement(name="positive", 

115 coltype=Boolean(), 

116 value=self.is_positive(), 

117 comment="FAST positive?"), 

118 ] 

119 

120 def is_complete(self) -> bool: 

121 return ( 

122 self.all_fields_not_none(self.TASK_FIELDS) and 

123 self.field_contents_valid() 

124 ) 

125 

126 def total_score(self) -> int: 

127 return self.sum_fields(self.TASK_FIELDS) 

128 

129 # noinspection PyUnresolvedReferences 

130 def is_positive(self) -> bool: 

131 if self.q1 is not None: 

132 if self.q1 == 0: 

133 return False 

134 if self.q1 >= 3: 

135 return True 

136 return self.total_score() >= 3 

137 

138 # noinspection PyUnresolvedReferences 

139 def get_task_html(self, req: CamcopsRequest) -> str: 

140 main_dict = { 

141 None: None, 

142 0: "0 — " + self.wxstring(req, "q1to3_option0"), 

143 1: "1 — " + self.wxstring(req, "q1to3_option1"), 

144 2: "2 — " + self.wxstring(req, "q1to3_option2"), 

145 3: "3 — " + self.wxstring(req, "q1to3_option3"), 

146 4: "4 — " + self.wxstring(req, "q1to3_option4"), 

147 } 

148 q4_dict = { 

149 None: None, 

150 0: "0 — " + self.wxstring(req, "q4_option0"), 

151 2: "2 — " + self.wxstring(req, "q4_option2"), 

152 4: "4 — " + self.wxstring(req, "q4_option4"), 

153 } 

154 q_a = tr_qa(self.wxstring(req, "q1"), get_from_dict(main_dict, self.q1)) # noqa 

155 q_a += tr_qa(self.wxstring(req, "q2"), get_from_dict(main_dict, self.q2)) # noqa 

156 q_a += tr_qa(self.wxstring(req, "q3"), get_from_dict(main_dict, self.q3)) # noqa 

157 q_a += tr_qa(self.wxstring(req, "q4"), get_from_dict(q4_dict, self.q4)) 

158 

159 tr_total_score = tr( 

160 req.sstring(SS.TOTAL_SCORE), 

161 answer(self.total_score()) + f" / {self.MAX_SCORE}" 

162 ) 

163 tr_positive = tr_qa( 

164 self.wxstring(req, "positive") + " <sup>[1]</sup>", 

165 get_yes_no(req, self.is_positive()) 

166 ) 

167 return f""" 

168 <div class="{CssClass.SUMMARY}"> 

169 <table class="{CssClass.SUMMARY}"> 

170 {self.get_is_complete_tr(req)} 

171 {tr_total_score} 

172 {tr_positive} 

173 </table> 

174 </div> 

175 <table class="{CssClass.TASKDETAIL}"> 

176 <tr> 

177 <th width="60%">Question</th> 

178 <th width="40%">Answer</th> 

179 </tr> 

180 {q_a} 

181 </table> 

182 <div class="{CssClass.FOOTNOTES}"> 

183 [1] Negative if Q1 = 0. Positive if Q1 ≥ 3. Otherwise positive 

184 if total score ≥ 3. 

185 </div> 

186 """ 

187 

188 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: 

189 codes = [SnomedExpression(req.snomed(SnomedLookup.FAST_PROCEDURE_ASSESSMENT))] # noqa 

190 if self.is_complete(): 

191 codes.append(SnomedExpression( 

192 req.snomed(SnomedLookup.FAST_SCALE), 

193 { 

194 req.snomed(SnomedLookup.FAST_SCORE): self.total_score(), 

195 } 

196 )) 

197 return codes