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

29import logging 

30from typing import Dict, List, Optional, Type 

31 

32from cardinal_pythonlib.classes import classproperty 

33from cardinal_pythonlib.stringfunc import strseq 

34from semantic_version import Version 

35from sqlalchemy.sql.sqltypes import Integer 

36 

37from camcops_server.cc_modules.cc_constants import CssClass 

38from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE 

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

40from camcops_server.cc_modules.cc_report import ( 

41 AverageScoreReport, 

42 ScoreDetails, 

43) 

44from camcops_server.cc_modules.cc_request import CamcopsRequest 

45from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

46from camcops_server.cc_modules.cc_sqla_coltypes import ( 

47 CamcopsColumn, 

48 ZERO_TO_FOUR_CHECKER, 

49) 

50from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

51from camcops_server.cc_modules.cc_task import ( 

52 get_from_dict, 

53 Task, 

54 TaskHasPatientMixin, 

55) 

56from camcops_server.cc_modules.cc_trackerhelpers import ( 

57 TrackerAxisTick, 

58 TrackerInfo, 

59) 

60 

61log = logging.getLogger(__name__) 

62 

63 

64# ============================================================================= 

65# CORE-10 

66# ============================================================================= 

67 

68class Core10(TaskHasPatientMixin, Task): 

69 """ 

70 Server implementation of the CORE-10 task. 

71 """ 

72 __tablename__ = "core10" 

73 shortname = "CORE-10" 

74 provides_trackers = True 

75 

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

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

78 

79 q1 = CamcopsColumn( 

80 "q1", Integer, 

81 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

82 comment="Q1 (tension/anxiety)" + COMMENT_NORMAL 

83 ) 

84 q2 = CamcopsColumn( 

85 "q2", Integer, 

86 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

87 comment="Q2 (support)" + COMMENT_REVERSED 

88 ) 

89 q3 = CamcopsColumn( 

90 "q3", Integer, 

91 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

92 comment="Q3 (coping)" + COMMENT_REVERSED 

93 ) 

94 q4 = CamcopsColumn( 

95 "q4", Integer, 

96 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

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

98 ) 

99 q5 = CamcopsColumn( 

100 "q5", Integer, 

101 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

102 comment="Q5 (panic)" + COMMENT_NORMAL 

103 ) 

104 q6 = CamcopsColumn( 

105 "q6", Integer, 

106 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

107 comment="Q6 (suicidality)" + COMMENT_NORMAL 

108 ) 

109 q7 = CamcopsColumn( 

110 "q7", Integer, 

111 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

112 comment="Q7 (sleep problems)" + COMMENT_NORMAL 

113 ) 

114 q8 = CamcopsColumn( 

115 "q8", Integer, 

116 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

117 comment="Q8 (despair/hopelessness)" + COMMENT_NORMAL 

118 ) 

119 q9 = CamcopsColumn( 

120 "q9", Integer, 

121 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

122 comment="Q9 (unhappy)" + COMMENT_NORMAL 

123 ) 

124 q10 = CamcopsColumn( 

125 "q10", Integer, 

126 permitted_value_checker=ZERO_TO_FOUR_CHECKER, 

127 comment="Q10 (unwanted images)" + COMMENT_NORMAL 

128 ) 

129 

130 N_QUESTIONS = 10 

131 MAX_SCORE = 4 * N_QUESTIONS 

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

133 

134 @staticmethod 

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

136 _ = req.gettext 

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

138 

139 # noinspection PyMethodParameters 

140 @classproperty 

141 def minimum_client_version(cls) -> Version: 

142 return Version("2.2.8") 

143 

144 def is_complete(self) -> bool: 

145 return self.all_fields_not_none(self.QUESTION_FIELDNAMES) 

146 

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

148 return [TrackerInfo( 

149 value=self.clinical_score(), 

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

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

152 axis_min=-0.5, 

153 axis_max=self.MAX_SCORE + 0.5, 

154 axis_ticks=[ 

155 TrackerAxisTick(40, "40"), 

156 TrackerAxisTick(35, "35"), 

157 TrackerAxisTick(30, "30"), 

158 TrackerAxisTick(25, "25"), 

159 TrackerAxisTick(20, "20"), 

160 TrackerAxisTick(15, "15"), 

161 TrackerAxisTick(10, "10"), 

162 TrackerAxisTick(5, "5"), 

163 TrackerAxisTick(0, "0"), 

164 ], 

165 horizontal_lines=[ 

166 30, 

167 20, 

168 10, 

169 ], 

170 )] 

171 

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

173 if not self.is_complete(): 

174 return CTV_INCOMPLETE 

175 return [CtvInfo(content=( 

176 f"CORE-10 clinical score {self.clinical_score()}/{self.MAX_SCORE}" 

177 ))] 

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

179 

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

181 return self.standard_task_summary_fields() + [ 

182 SummaryElement( 

183 name="clinical_score", coltype=Integer(), 

184 value=self.clinical_score(), 

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

186 ] 

187 

188 def total_score(self) -> int: 

189 return self.sum_fields(self.QUESTION_FIELDNAMES) 

190 

191 def n_questions_complete(self) -> int: 

192 return self.n_fields_not_none(self.QUESTION_FIELDNAMES) 

193 

194 def clinical_score(self) -> float: 

195 n_q_completed = self.n_questions_complete() 

196 if n_q_completed == 0: 

197 # avoid division by zero 

198 return 0 

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

200 

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

202 normal_dict = { 

203 None: None, 

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

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

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

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

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

209 } 

210 reversed_dict = { 

211 None: None, 

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

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

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

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

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

217 } 

218 

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

220 nstr = str(qnum_) 

221 return tr_qa(self.wxstring(req, "q" + nstr), 

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

223 

224 q_a = get_tr_qa(1, normal_dict) 

225 for qnum in [2, 3]: 

226 q_a += get_tr_qa(qnum, reversed_dict) 

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

228 q_a += get_tr_qa(qnum, normal_dict) 

229 

230 tr_clinical_score = tr( 

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

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

233 ) 

234 return f""" 

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

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

237 {self.get_is_complete_tr(req)} 

238 {tr_clinical_score} 

239 </table> 

240 </div> 

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

242 Ratings are over the last week. 

243 </div> 

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

245 <tr> 

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

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

248 </tr> 

249 {q_a} 

250 </table> 

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

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

253 ÷ number of questions completed. If all questions are 

254 completed, it's just the total score. 

255 </div> 

256 """ 

257 

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

259 codes = [SnomedExpression(req.snomed(SnomedLookup.CORE10_PROCEDURE_ASSESSMENT))] # noqa 

260 if self.is_complete(): 

261 codes.append(SnomedExpression( 

262 req.snomed(SnomedLookup.CORE10_SCALE), 

263 { 

264 req.snomed(SnomedLookup.CORE10_SCORE): self.total_score(), 

265 } 

266 )) 

267 return codes 

268 

269 

270class Core10Report(AverageScoreReport): 

271 """ 

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

273 an average final measure and an average progress score. 

274 """ 

275 # noinspection PyMethodParameters 

276 @classproperty 

277 def report_id(cls) -> str: 

278 return "core10" 

279 

280 @classmethod 

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

282 _ = req.gettext 

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

284 

285 # noinspection PyMethodParameters 

286 @classproperty 

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

288 return Core10 

289 

290 @classmethod 

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

292 _ = req.gettext 

293 return [ 

294 ScoreDetails( 

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

296 scorefunc=Core10.clinical_score, 

297 minimum=0, 

298 maximum=Core10.MAX_SCORE, 

299 higher_score_is_better=False 

300 ) 

301 ]