Coverage for tasks/cesdr.py: 52%

111 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/cesdr.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.classes import classproperty 

33from cardinal_pythonlib.stringfunc import strseq 

34from semantic_version import Version 

35from sqlalchemy.ext.declarative import DeclarativeMeta 

36from sqlalchemy.sql.sqltypes import Boolean 

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_db import add_multiple_columns 

41from camcops_server.cc_modules.cc_html import get_yes_no, tr, tr_qa 

42from camcops_server.cc_modules.cc_request import CamcopsRequest 

43 

44from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

45from camcops_server.cc_modules.cc_task import ( 

46 get_from_dict, 

47 Task, 

48 TaskHasPatientMixin, 

49) 

50from camcops_server.cc_modules.cc_text import SS 

51from camcops_server.cc_modules.cc_trackerhelpers import ( 

52 equally_spaced_int, 

53 regular_tracker_axis_ticks_int, 

54 TrackerInfo, 

55 TrackerLabel, 

56) 

57 

58 

59# ============================================================================= 

60# CESD-R 

61# ============================================================================= 

62 

63 

64class CesdrMetaclass(DeclarativeMeta): 

65 """ 

66 There is a multilayer metaclass problem; see hads.py for discussion. 

67 """ 

68 

69 # noinspection PyInitNewSignature 

70 def __init__( 

71 cls: Type["Cesdr"], 

72 name: str, 

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

74 classdict: Dict[str, Any], 

75 ) -> None: 

76 add_multiple_columns( 

77 cls, 

78 "q", 

79 1, 

80 cls.N_QUESTIONS, 

81 minimum=0, 

82 maximum=4, 

83 comment_fmt=( 

84 "Q{n} ({s}) (0 not at all - " 

85 "4 nearly every day for two weeks)" 

86 ), 

87 comment_strings=[ 

88 "poor appetite", 

89 "unshakable blues", 

90 "poor concentration", 

91 "depressed", 

92 "sleep restless", 

93 "sad", 

94 "could not get going", 

95 "nothing made me happy", 

96 "felt a bad person", 

97 "loss of interest", 

98 "oversleeping", 

99 "moving slowly", 

100 "fidgety", 

101 "wished were dead", 

102 "wanted to hurt self", 

103 "tiredness", 

104 "disliked self", 

105 "unintended weight loss", 

106 "difficulty getting to sleep", 

107 "lack of focus", 

108 ], 

109 ) 

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

111 

112 

113class Cesdr(TaskHasPatientMixin, Task, metaclass=CesdrMetaclass): 

114 """ 

115 Server implementation of the CESD task. 

116 """ 

117 

118 __tablename__ = "cesdr" 

119 shortname = "CESD-R" 

120 info_filename_stem = "cesd" 

121 provides_trackers = True 

122 

123 CAT_NONCLINICAL = 0 

124 CAT_SUB = 1 

125 CAT_POSS_MAJOR = 2 

126 CAT_PROB_MAJOR = 3 

127 CAT_MAJOR = 4 

128 

129 DEPRESSION_RISK_THRESHOLD = 16 

130 

131 FREQ_NOT_AT_ALL = 0 

132 FREQ_1_2_DAYS_LAST_WEEK = 1 

133 FREQ_3_4_DAYS_LAST_WEEK = 2 

134 FREQ_5_7_DAYS_LAST_WEEK = 3 

135 FREQ_DAILY_2_WEEKS = 4 

136 

137 N_QUESTIONS = 20 

138 N_ANSWERS = 5 

139 

140 POSS_MAJOR_THRESH = 2 

141 PROB_MAJOR_THRESH = 3 

142 MAJOR_THRESH = 4 

143 

144 SCORED_FIELDS = strseq("q", 1, N_QUESTIONS) 

145 TASK_FIELDS = SCORED_FIELDS 

146 MIN_SCORE = 0 

147 MAX_SCORE = 3 * N_QUESTIONS 

148 

149 @staticmethod 

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

151 _ = req.gettext 

152 return _("Center for Epidemiologic Studies Depression Scale (Revised)") 

153 

154 # noinspection PyMethodParameters 

155 @classproperty 

156 def minimum_client_version(cls) -> Version: 

157 return Version("2.2.8") 

158 

159 def is_complete(self) -> bool: 

160 return ( 

161 self.all_fields_not_none(self.TASK_FIELDS) 

162 and self.field_contents_valid() 

163 ) 

164 

165 def total_score(self) -> int: 

166 return self.sum_fields(self.SCORED_FIELDS) - self.count_where( 

167 self.SCORED_FIELDS, [self.FREQ_DAILY_2_WEEKS] 

168 ) 

169 

170 def get_depression_category(self) -> int: 

171 

172 if not self.has_depression_risk(): 

173 return self.CAT_SUB 

174 

175 q_group_anhedonia = [8, 10] 

176 q_group_dysphoria = [2, 4, 6] 

177 other_q_groups = { 

178 "appetite": [1, 18], 

179 "sleep": [5, 11, 19], 

180 "thinking": [3, 20], 

181 "guilt": [9, 17], 

182 "tired": [7, 16], 

183 "movement": [12, 13], 

184 "suicidal": [14, 15], 

185 } 

186 

187 # Dysphoria or anhedonia must be present at frequency 

188 # FREQ_DAILY_2_WEEKS 

189 anhedonia_criterion = self.fulfils_group_criteria( 

190 q_group_anhedonia, True 

191 ) or self.fulfils_group_criteria(q_group_dysphoria, True) 

192 if anhedonia_criterion: 

193 category_count_high_freq = 0 

194 category_count_lower_freq = 0 

195 for qgroup in other_q_groups.values(): 

196 if self.fulfils_group_criteria(qgroup, True): 

197 # Category contains an answer == FREQ_DAILY_2_WEEKS 

198 category_count_high_freq += 1 

199 if self.fulfils_group_criteria(qgroup, False): 

200 # Category contains an answer == FREQ_DAILY_2_WEEKS or 

201 # FREQ_5_7_DAYS_LAST_WEEK 

202 category_count_lower_freq += 1 

203 

204 if category_count_high_freq >= self.MAJOR_THRESH: 

205 # Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS) 

206 # plus 4 other symptom groups at FREQ_DAILY_2_WEEKS 

207 return self.CAT_MAJOR 

208 if category_count_lower_freq >= self.PROB_MAJOR_THRESH: 

209 # Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS) 

210 # plus 3 other symptom groups at FREQ_DAILY_2_WEEKS or 

211 # FREQ_5_7_DAYS_LAST_WEEK 

212 return self.CAT_PROB_MAJOR 

213 if category_count_lower_freq >= self.POSS_MAJOR_THRESH: 

214 # Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS) 

215 # plus 2 other symptom groups at FREQ_DAILY_2_WEEKS or 

216 # FREQ_5_7_DAYS_LAST_WEEK 

217 return self.CAT_POSS_MAJOR 

218 

219 if self.has_depression_risk(): 

220 # Total CESD-style score >= 16 but doesn't meet other criteria. 

221 return self.CAT_SUB 

222 

223 return self.CAT_NONCLINICAL 

224 

225 def fulfils_group_criteria( 

226 self, qnums: List[int], nearly_every_day_2w: bool 

227 ) -> bool: 

228 qstrings = ["q" + str(qnum) for qnum in qnums] 

229 if nearly_every_day_2w: 

230 possible_values = [self.FREQ_DAILY_2_WEEKS] 

231 else: 

232 possible_values = [ 

233 self.FREQ_5_7_DAYS_LAST_WEEK, 

234 self.FREQ_DAILY_2_WEEKS, 

235 ] 

236 count = self.count_where(qstrings, possible_values) 

237 return count > 0 

238 

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

240 line_step = 20 

241 threshold_line = self.DEPRESSION_RISK_THRESHOLD - 0.5 

242 # noinspection PyTypeChecker 

243 return [ 

244 TrackerInfo( 

245 value=self.total_score(), 

246 plot_label="CESD-R total score", 

247 axis_label=f"Total score ({self.MIN_SCORE}-{self.MAX_SCORE})", 

248 axis_min=self.MIN_SCORE - 0.5, 

249 axis_max=self.MAX_SCORE + 0.5, 

250 axis_ticks=regular_tracker_axis_ticks_int( 

251 self.MIN_SCORE, self.MAX_SCORE, step=line_step 

252 ), 

253 horizontal_lines=equally_spaced_int( 

254 self.MIN_SCORE + line_step, 

255 self.MAX_SCORE - line_step, 

256 step=line_step, 

257 ) 

258 + [threshold_line], 

259 horizontal_labels=[ 

260 TrackerLabel( 

261 threshold_line, 

262 self.wxstring(req, "depression_or_risk_of"), 

263 ) 

264 ], 

265 ) 

266 ] 

267 

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

269 if not self.is_complete(): 

270 return CTV_INCOMPLETE 

271 return [CtvInfo(content=f"CESD-R total score {self.total_score()}")] 

272 

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

274 return self.standard_task_summary_fields() + [ 

275 SummaryElement( 

276 name="depression_risk", 

277 coltype=Boolean(), 

278 value=self.has_depression_risk(), 

279 comment="Has depression or at risk of depression", 

280 ) 

281 ] 

282 

283 def has_depression_risk(self) -> bool: 

284 return self.total_score() >= self.DEPRESSION_RISK_THRESHOLD 

285 

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

287 score = self.total_score() 

288 answer_dict = {None: None} 

289 for option in range(self.N_ANSWERS): 

290 answer_dict[option] = ( 

291 str(option) + " – " + self.wxstring(req, "a" + str(option)) 

292 ) 

293 q_a = "" 

294 for q in range(1, self.N_QUESTIONS): 

295 q_a += tr_qa( 

296 self.wxstring(req, "q" + str(q) + "_s"), 

297 get_from_dict(answer_dict, getattr(self, "q" + str(q))), 

298 ) 

299 

300 tr_total_score = tr_qa(f"{req.sstring(SS.TOTAL_SCORE)} (0–60)", score) 

301 tr_depression_or_risk_of = tr_qa( 

302 self.wxstring(req, "depression_or_risk_of") + "? <sup>[1]</sup>", 

303 get_yes_no(req, self.has_depression_risk()), 

304 ) 

305 tr_provisional_diagnosis = tr( 

306 "Provisional diagnosis <sup>[2]</sup>", 

307 self.wxstring( 

308 req, "category_" + str(self.get_depression_category()) 

309 ), 

310 ) 

311 return f""" 

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

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

314 {self.get_is_complete_tr(req)} 

315 {tr_total_score} 

316 {tr_depression_or_risk_of} 

317 {tr_provisional_diagnosis} 

318 </table> 

319 </div> 

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

321 <tr> 

322 <th width="70%">Question</th> 

323 <th width="30%">Answer</th> 

324 </tr> 

325 {q_a} 

326 </table> 

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

328 [1] Presence of depression (or depression risk) is indicated by a 

329 score &ge; 16 

330 [2] Diagnostic criteria described at 

331 <a href="https://cesd-r.com/cesdr/">https://cesd-r.com/cesdr/</a> 

332 </div> 

333 """ # noqa