Coverage for tasks/core10.py: 61%

96 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/core10.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 

30import logging 

31from typing import Dict, List, Optional, Type 

32 

33from cardinal_pythonlib.classes import classproperty 

34from cardinal_pythonlib.stringfunc import strseq 

35from semantic_version import Version 

36from sqlalchemy.sql.sqltypes import Integer 

37 

38from camcops_server.cc_modules.cc_constants import CssClass 

39from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE 

40from camcops_server.cc_modules.cc_html import answer, tr, tr_qa 

41from camcops_server.cc_modules.cc_report import ( 

42 AverageScoreReport, 

43 ScoreDetails, 

44) 

45from camcops_server.cc_modules.cc_request import CamcopsRequest 

46from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

47from camcops_server.cc_modules.cc_sqla_coltypes import ( 

48 CamcopsColumn, 

49 ZERO_TO_FOUR_CHECKER, 

50) 

51from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

52from camcops_server.cc_modules.cc_task import ( 

53 get_from_dict, 

54 Task, 

55 TaskHasPatientMixin, 

56) 

57from camcops_server.cc_modules.cc_trackerhelpers import ( 

58 TrackerAxisTick, 

59 TrackerInfo, 

60) 

61 

62log = logging.getLogger(__name__) 

63 

64 

65# ============================================================================= 

66# CORE-10 

67# ============================================================================= 

68 

69 

70class Core10(TaskHasPatientMixin, Task): 

71 """ 

72 Server implementation of the CORE-10 task. 

73 """ 

74 

75 __tablename__ = "core10" 

76 shortname = "CORE-10" 

77 provides_trackers = True 

78 

79 COMMENT_NORMAL = " (0 not at all - 4 most or all of the time)" 

80 COMMENT_REVERSED = " (0 most or all of the time - 4 not at all)" 

81 

82 q1 = CamcopsColumn( 

83 "q1", 

84 Integer, 

85 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

86 comment="Q1 (tension/anxiety)" + COMMENT_NORMAL, 

87 ) 

88 q2 = CamcopsColumn( 

89 "q2", 

90 Integer, 

91 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

92 comment="Q2 (support)" + COMMENT_REVERSED, 

93 ) 

94 q3 = CamcopsColumn( 

95 "q3", 

96 Integer, 

97 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

98 comment="Q3 (coping)" + COMMENT_REVERSED, 

99 ) 

100 q4 = CamcopsColumn( 

101 "q4", 

102 Integer, 

103 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

104 comment="Q4 (talking is too much)" + COMMENT_NORMAL, 

105 ) 

106 q5 = CamcopsColumn( 

107 "q5", 

108 Integer, 

109 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

110 comment="Q5 (panic)" + COMMENT_NORMAL, 

111 ) 

112 q6 = CamcopsColumn( 

113 "q6", 

114 Integer, 

115 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

116 comment="Q6 (suicidality)" + COMMENT_NORMAL, 

117 ) 

118 q7 = CamcopsColumn( 

119 "q7", 

120 Integer, 

121 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

122 comment="Q7 (sleep problems)" + COMMENT_NORMAL, 

123 ) 

124 q8 = CamcopsColumn( 

125 "q8", 

126 Integer, 

127 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

128 comment="Q8 (despair/hopelessness)" + COMMENT_NORMAL, 

129 ) 

130 q9 = CamcopsColumn( 

131 "q9", 

132 Integer, 

133 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

134 comment="Q9 (unhappy)" + COMMENT_NORMAL, 

135 ) 

136 q10 = CamcopsColumn( 

137 "q10", 

138 Integer, 

139 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

140 comment="Q10 (unwanted images)" + COMMENT_NORMAL, 

141 ) 

142 

143 N_QUESTIONS = 10 

144 MAX_SCORE = 4 * N_QUESTIONS 

145 QUESTION_FIELDNAMES = strseq("q", 1, N_QUESTIONS) 

146 

147 @staticmethod 

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

149 _ = req.gettext 

150 return _("Clinical Outcomes in Routine Evaluation, 10-item measure") 

151 

152 # noinspection PyMethodParameters 

153 @classproperty 

154 def minimum_client_version(cls) -> Version: 

155 return Version("2.2.8") 

156 

157 def is_complete(self) -> bool: 

158 return self.all_fields_not_none(self.QUESTION_FIELDNAMES) 

159 

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

161 return [ 

162 TrackerInfo( 

163 value=self.clinical_score(), 

164 plot_label="CORE-10 clinical score (rating distress)", 

165 axis_label=f"Clinical score (out of {self.MAX_SCORE})", 

166 axis_min=-0.5, 

167 axis_max=self.MAX_SCORE + 0.5, 

168 axis_ticks=[ 

169 TrackerAxisTick(40, "40"), 

170 TrackerAxisTick(35, "35"), 

171 TrackerAxisTick(30, "30"), 

172 TrackerAxisTick(25, "25"), 

173 TrackerAxisTick(20, "20"), 

174 TrackerAxisTick(15, "15"), 

175 TrackerAxisTick(10, "10"), 

176 TrackerAxisTick(5, "5"), 

177 TrackerAxisTick(0, "0"), 

178 ], 

179 horizontal_lines=[30, 20, 10], 

180 ) 

181 ] 

182 

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

184 if not self.is_complete(): 

185 return CTV_INCOMPLETE 

186 return [ 

187 CtvInfo( 

188 content=( 

189 f"CORE-10 clinical score " 

190 f"{self.clinical_score()}/{self.MAX_SCORE}" 

191 ) 

192 ) 

193 ] 

194 # todo: CORE10: add suicidality to clinical text? 

195 

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

197 return self.standard_task_summary_fields() + [ 

198 SummaryElement( 

199 name="clinical_score", 

200 coltype=Integer(), 

201 value=self.clinical_score(), 

202 comment=f"Clinical score (/{self.MAX_SCORE})", 

203 ) 

204 ] 

205 

206 def total_score(self) -> int: 

207 return self.sum_fields(self.QUESTION_FIELDNAMES) 

208 

209 def n_questions_complete(self) -> int: 

210 return self.n_fields_not_none(self.QUESTION_FIELDNAMES) 

211 

212 def clinical_score(self) -> float: 

213 n_q_completed = self.n_questions_complete() 

214 if n_q_completed == 0: 

215 # avoid division by zero 

216 return 0 

217 return self.N_QUESTIONS * self.total_score() / n_q_completed 

218 

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

220 normal_dict = { 

221 None: None, 

222 0: "0 — " + self.wxstring(req, "a0"), 

223 1: "1 — " + self.wxstring(req, "a1"), 

224 2: "2 — " + self.wxstring(req, "a2"), 

225 3: "3 — " + self.wxstring(req, "a3"), 

226 4: "4 — " + self.wxstring(req, "a4"), 

227 } 

228 reversed_dict = { 

229 None: None, 

230 0: "0 — " + self.wxstring(req, "a4"), 

231 1: "1 — " + self.wxstring(req, "a3"), 

232 2: "2 — " + self.wxstring(req, "a2"), 

233 3: "3 — " + self.wxstring(req, "a1"), 

234 4: "4 — " + self.wxstring(req, "a0"), 

235 } 

236 

237 def get_tr_qa(qnum_: int, mapping: Dict[Optional[int], str]) -> str: 

238 nstr = str(qnum_) 

239 return tr_qa( 

240 self.wxstring(req, "q" + nstr), 

241 get_from_dict(mapping, getattr(self, "q" + nstr)), 

242 ) 

243 

244 q_a = get_tr_qa(1, normal_dict) 

245 for qnum in (2, 3): 

246 q_a += get_tr_qa(qnum, reversed_dict) 

247 for qnum in range(4, self.N_QUESTIONS + 1): 

248 q_a += get_tr_qa(qnum, normal_dict) 

249 

250 tr_clinical_score = tr( 

251 "Clinical score <sup>[1]</sup>", 

252 answer(self.clinical_score()) + " / {}".format(self.MAX_SCORE), 

253 ) 

254 return f""" 

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

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

257 {self.get_is_complete_tr(req)} 

258 {tr_clinical_score} 

259 </table> 

260 </div> 

261 <div class="{CssClass.EXPLANATION}"> 

262 Ratings are over the last week. 

263 </div> 

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

265 <tr> 

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

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

268 </tr> 

269 {q_a} 

270 </table> 

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

272 [1] Clinical score is: number of questions × total score 

273 ÷ number of questions completed. If all questions are 

274 completed, it's just the total score. 

275 </div> 

276 """ 

277 

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

279 codes = [ 

280 SnomedExpression( 

281 req.snomed(SnomedLookup.CORE10_PROCEDURE_ASSESSMENT) 

282 ) 

283 ] 

284 if self.is_complete(): 

285 codes.append( 

286 SnomedExpression( 

287 req.snomed(SnomedLookup.CORE10_SCALE), 

288 { 

289 req.snomed( 

290 SnomedLookup.CORE10_SCORE 

291 ): self.total_score() 

292 }, 

293 ) 

294 ) 

295 return codes 

296 

297 

298class Core10Report(AverageScoreReport): 

299 """ 

300 An average score of the people seen at the start of treatment 

301 an average final measure and an average progress score. 

302 """ 

303 

304 # noinspection PyMethodParameters 

305 @classproperty 

306 def report_id(cls) -> str: 

307 return "core10" 

308 

309 @classmethod 

310 def title(cls, req: "CamcopsRequest") -> str: 

311 _ = req.gettext 

312 return _("CORE-10 — Average scores") 

313 

314 # noinspection PyMethodParameters 

315 @classproperty 

316 def task_class(cls) -> Type[Task]: 

317 return Core10 

318 

319 @classmethod 

320 def scoretypes(cls, req: "CamcopsRequest") -> List[ScoreDetails]: 

321 _ = req.gettext 

322 return [ 

323 ScoreDetails( 

324 name=_("CORE-10 clinical score"), 

325 scorefunc=Core10.clinical_score, 

326 minimum=0, 

327 maximum=Core10.MAX_SCORE, 

328 higher_score_is_better=False, 

329 ) 

330 ]