Coverage for tasks/fast.py: 52%

61 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

2camcops_server/tasks/fast.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CamCOPS. 

10 

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

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

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

14 (at your option) any later version. 

15 

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

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

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

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

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

23 

24=============================================================================== 

25 

26""" 

27 

28from typing import Any, cast, List, Type 

29 

30from cardinal_pythonlib.stringfunc import strseq 

31from sqlalchemy.sql.sqltypes import Boolean, Integer 

32 

33from camcops_server.cc_modules.cc_constants import CssClass 

34from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

35from camcops_server.cc_modules.cc_db import add_multiple_columns 

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

37from camcops_server.cc_modules.cc_request import CamcopsRequest 

38from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

39from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

40from camcops_server.cc_modules.cc_task import ( 

41 get_from_dict, 

42 Task, 

43 TaskHasPatientMixin, 

44) 

45from camcops_server.cc_modules.cc_text import SS 

46from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

47 

48 

49# ============================================================================= 

50# FAST 

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

52 

53 

54class Fast( # type: ignore[misc] 

55 TaskHasPatientMixin, 

56 Task, 

57): 

58 """ 

59 Server implementation of the FAST task. 

60 """ 

61 

62 __tablename__ = "fast" 

63 shortname = "FAST" 

64 

65 NQUESTIONS = 4 

66 

67 @classmethod 

68 def extend_columns(cls: Type["Fast"], **kwargs: Any) -> None: 

69 add_multiple_columns( 

70 cls, 

71 "q", 

72 1, 

73 cls.NQUESTIONS, 

74 minimum=0, 

75 maximum=4, 

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

77 comment_strings=[ 

78 "M>8, F>6 drinks", 

79 "unable to remember", 

80 "failed to do what was expected", 

81 "others concerned", 

82 ], 

83 ) 

84 

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

86 MAX_SCORE = 16 

87 

88 @staticmethod 

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

90 _ = req.gettext 

91 return _("Fast Alcohol Screening Test") 

92 

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

94 return [ 

95 TrackerInfo( 

96 value=self.total_score(), 

97 plot_label="FAST total score", 

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

99 axis_min=-0.5, 

100 axis_max=self.MAX_SCORE + 0.5, 

101 ) 

102 ] 

103 

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

105 if not self.is_complete(): 

106 return CTV_INCOMPLETE 

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

108 return [ 

109 CtvInfo( 

110 content=( 

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

112 f"({classification})" 

113 ) 

114 ) 

115 ] 

116 

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

118 return self.standard_task_summary_fields() + [ 

119 SummaryElement( 

120 name="total", 

121 coltype=Integer(), 

122 value=self.total_score(), 

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

124 ), 

125 SummaryElement( 

126 name="positive", 

127 coltype=Boolean(), 

128 value=self.is_positive(), 

129 comment="FAST positive?", 

130 ), 

131 ] 

132 

133 def is_complete(self) -> bool: 

134 return ( 

135 self.all_fields_not_none(self.TASK_FIELDS) 

136 and self.field_contents_valid() 

137 ) 

138 

139 def total_score(self) -> int: 

140 return cast(int, self.sum_fields(self.TASK_FIELDS)) 

141 

142 # noinspection PyUnresolvedReferences 

143 def is_positive(self) -> bool: 

144 if self.q1 is not None: # type: ignore[attr-defined] 

145 if self.q1 == 0: # type: ignore[attr-defined] 

146 return False 

147 if self.q1 >= 3: # type: ignore[attr-defined] 

148 return True 

149 return self.total_score() >= 3 

150 

151 # noinspection PyUnresolvedReferences 

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

153 main_dict = { 

154 None: None, 

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

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

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

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

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

160 } 

161 q4_dict = { 

162 None: None, 

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

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

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

166 } 

167 q_a = tr_qa( 

168 self.wxstring(req, "q1"), get_from_dict(main_dict, self.q1) # type: ignore[attr-defined] # noqa: E501 

169 ) 

170 q_a += tr_qa( 

171 self.wxstring(req, "q2"), get_from_dict(main_dict, self.q2) # type: ignore[attr-defined] # noqa: E501 

172 ) 

173 q_a += tr_qa( 

174 self.wxstring(req, "q3"), get_from_dict(main_dict, self.q3) # type: ignore[attr-defined] # noqa: E501 

175 ) 

176 q_a += tr_qa(self.wxstring(req, "q4"), get_from_dict(q4_dict, self.q4)) # type: ignore[attr-defined] # noqa: E501 

177 

178 tr_total_score = tr( 

179 req.sstring(SS.TOTAL_SCORE), 

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

181 ) 

182 tr_positive = tr_qa( 

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

184 get_yes_no(req, self.is_positive()), 

185 ) 

186 return f""" 

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

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

189 {self.get_is_complete_tr(req)} 

190 {tr_total_score} 

191 {tr_positive} 

192 </table> 

193 </div> 

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

195 <tr> 

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

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

198 </tr> 

199 {q_a} 

200 </table> 

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

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

203 if total score ≥ 3. 

204 </div> 

205 """ 

206 

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

208 codes = [ 

209 SnomedExpression( 

210 req.snomed(SnomedLookup.FAST_PROCEDURE_ASSESSMENT) 

211 ) 

212 ] 

213 if self.is_complete(): 

214 codes.append( 

215 SnomedExpression( 

216 req.snomed(SnomedLookup.FAST_SCALE), 

217 {req.snomed(SnomedLookup.FAST_SCORE): self.total_score()}, 

218 ) 

219 ) 

220 return codes