Coverage for tasks/fast.py: 55%

64 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/fast.py 

5 

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

7 

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

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

10 

11 This file is part of CamCOPS. 

12 

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

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

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

16 (at your option) any later version. 

17 

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

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

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

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

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

25 

26=============================================================================== 

27 

28""" 

29 

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

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.sqltypes import Boolean, Integer 

35 

36from camcops_server.cc_modules.cc_constants import CssClass 

37from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

38from camcops_server.cc_modules.cc_db import add_multiple_columns 

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

40from camcops_server.cc_modules.cc_request import CamcopsRequest 

41from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

42from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

43from camcops_server.cc_modules.cc_task import ( 

44 get_from_dict, 

45 Task, 

46 TaskHasPatientMixin, 

47) 

48from camcops_server.cc_modules.cc_text import SS 

49from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

50 

51 

52# ============================================================================= 

53# FAST 

54# ============================================================================= 

55 

56 

57class FastMetaclass(DeclarativeMeta): 

58 # noinspection PyInitNewSignature 

59 def __init__( 

60 cls: Type["Fast"], 

61 name: str, 

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

63 classdict: Dict[str, Any], 

64 ) -> None: 

65 add_multiple_columns( 

66 cls, 

67 "q", 

68 1, 

69 cls.NQUESTIONS, 

70 minimum=0, 

71 maximum=4, 

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

73 comment_strings=[ 

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

75 "unable to remember", 

76 "failed to do what was expected", 

77 "others concerned", 

78 ], 

79 ) 

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

81 

82 

83class Fast(TaskHasPatientMixin, Task, metaclass=FastMetaclass): 

84 """ 

85 Server implementation of the FAST task. 

86 """ 

87 

88 __tablename__ = "fast" 

89 shortname = "FAST" 

90 

91 NQUESTIONS = 4 

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

93 MAX_SCORE = 16 

94 

95 @staticmethod 

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

97 _ = req.gettext 

98 return _("Fast Alcohol Screening Test") 

99 

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

101 return [ 

102 TrackerInfo( 

103 value=self.total_score(), 

104 plot_label="FAST total score", 

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

106 axis_min=-0.5, 

107 axis_max=self.MAX_SCORE + 0.5, 

108 ) 

109 ] 

110 

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

112 if not self.is_complete(): 

113 return CTV_INCOMPLETE 

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

115 return [ 

116 CtvInfo( 

117 content=( 

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

119 f"({classification})" 

120 ) 

121 ) 

122 ] 

123 

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

125 return self.standard_task_summary_fields() + [ 

126 SummaryElement( 

127 name="total", 

128 coltype=Integer(), 

129 value=self.total_score(), 

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

131 ), 

132 SummaryElement( 

133 name="positive", 

134 coltype=Boolean(), 

135 value=self.is_positive(), 

136 comment="FAST positive?", 

137 ), 

138 ] 

139 

140 def is_complete(self) -> bool: 

141 return ( 

142 self.all_fields_not_none(self.TASK_FIELDS) 

143 and self.field_contents_valid() 

144 ) 

145 

146 def total_score(self) -> int: 

147 return self.sum_fields(self.TASK_FIELDS) 

148 

149 # noinspection PyUnresolvedReferences 

150 def is_positive(self) -> bool: 

151 if self.q1 is not None: 

152 if self.q1 == 0: 

153 return False 

154 if self.q1 >= 3: 

155 return True 

156 return self.total_score() >= 3 

157 

158 # noinspection PyUnresolvedReferences 

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

160 main_dict = { 

161 None: None, 

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

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

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

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

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

167 } 

168 q4_dict = { 

169 None: None, 

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

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

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

173 } 

174 q_a = tr_qa( 

175 self.wxstring(req, "q1"), get_from_dict(main_dict, self.q1) 

176 ) 

177 q_a += tr_qa( 

178 self.wxstring(req, "q2"), get_from_dict(main_dict, self.q2) 

179 ) 

180 q_a += tr_qa( 

181 self.wxstring(req, "q3"), get_from_dict(main_dict, self.q3) 

182 ) 

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

184 

185 tr_total_score = tr( 

186 req.sstring(SS.TOTAL_SCORE), 

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

188 ) 

189 tr_positive = tr_qa( 

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

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

192 ) 

193 return f""" 

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

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

196 {self.get_is_complete_tr(req)} 

197 {tr_total_score} 

198 {tr_positive} 

199 </table> 

200 </div> 

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

202 <tr> 

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

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

205 </tr> 

206 {q_a} 

207 </table> 

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

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

210 if total score ≥ 3. 

211 </div> 

212 """ 

213 

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

215 codes = [ 

216 SnomedExpression( 

217 req.snomed(SnomedLookup.FAST_PROCEDURE_ASSESSMENT) 

218 ) 

219 ] 

220 if self.is_complete(): 

221 codes.append( 

222 SnomedExpression( 

223 req.snomed(SnomedLookup.FAST_SCALE), 

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

225 ) 

226 ) 

227 return codes