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

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

30 

31from cardinal_pythonlib.classes import classproperty 

32from cardinal_pythonlib.stringfunc import strseq 

33from semantic_version import Version 

34from sqlalchemy.ext.declarative import DeclarativeMeta 

35from sqlalchemy.sql.sqltypes import Boolean 

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

40from camcops_server.cc_modules.cc_html import ( 

41 get_yes_no, tr, tr_qa 

42) 

43from camcops_server.cc_modules.cc_request import CamcopsRequest 

44 

45from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

46from camcops_server.cc_modules.cc_task import ( 

47 get_from_dict, 

48 Task, 

49 TaskHasPatientMixin, 

50) 

51from camcops_server.cc_modules.cc_text import SS 

52from camcops_server.cc_modules.cc_trackerhelpers import ( 

53 equally_spaced_int, 

54 regular_tracker_axis_ticks_int, 

55 TrackerInfo, 

56 TrackerLabel, 

57) 

58 

59 

60# ============================================================================= 

61# CESD-R 

62# ============================================================================= 

63 

64class CesdrMetaclass(DeclarativeMeta): 

65 """ 

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

67 """ 

68 # noinspection PyInitNewSignature 

69 def __init__(cls: Type['Cesdr'], 

70 name: str, 

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

72 classdict: Dict[str, Any]) -> None: 

73 add_multiple_columns( 

74 cls, "q", 1, cls.N_QUESTIONS, 

75 minimum=0, maximum=4, 

76 comment_fmt=("Q{n} ({s}) (0 not at all - " 

77 "4 nearly every day for two weeks)"), 

78 comment_strings=[ 

79 "poor appetite", 

80 "unshakable blues", 

81 "poor concentration", 

82 "depressed", 

83 "sleep restless", 

84 "sad", 

85 "could not get going", 

86 "nothing made me happy", 

87 "felt a bad person", 

88 "loss of interest", 

89 "oversleeping", 

90 "moving slowly", 

91 "fidgety", 

92 "wished were dead", 

93 "wanted to hurt self", 

94 "tiredness", 

95 "disliked self", 

96 "unintended weight loss", 

97 "difficulty getting to sleep", 

98 "lack of focus", 

99 ] 

100 ) 

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

102 

103 

104class Cesdr(TaskHasPatientMixin, Task, 

105 metaclass=CesdrMetaclass): 

106 """ 

107 Server implementation of the CESD task. 

108 """ 

109 __tablename__ = 'cesdr' 

110 shortname = 'CESD-R' 

111 provides_trackers = True 

112 extrastring_taskname = "cesdr" 

113 

114 CAT_NONCLINICAL = 0 

115 CAT_SUB = 1 

116 CAT_POSS_MAJOR = 2 

117 CAT_PROB_MAJOR = 3 

118 CAT_MAJOR = 4 

119 

120 DEPRESSION_RISK_THRESHOLD = 16 

121 

122 FREQ_NOT_AT_ALL = 0 

123 FREQ_1_2_DAYS_LAST_WEEK = 1 

124 FREQ_3_4_DAYS_LAST_WEEK = 2 

125 FREQ_5_7_DAYS_LAST_WEEK = 3 

126 FREQ_DAILY_2_WEEKS = 4 

127 

128 N_QUESTIONS = 20 

129 N_ANSWERS = 5 

130 

131 POSS_MAJOR_THRESH = 2 

132 PROB_MAJOR_THRESH = 3 

133 MAJOR_THRESH = 4 

134 

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

136 TASK_FIELDS = SCORED_FIELDS 

137 MIN_SCORE = 0 

138 MAX_SCORE = 3 * N_QUESTIONS 

139 

140 @staticmethod 

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

142 _ = req.gettext 

143 return _('Center for Epidemiologic Studies Depression Scale (Revised)') 

144 

145 # noinspection PyMethodParameters 

146 @classproperty 

147 def minimum_client_version(cls) -> Version: 

148 return Version("2.2.8") 

149 

150 def is_complete(self) -> bool: 

151 return ( 

152 self.all_fields_not_none(self.TASK_FIELDS) and 

153 self.field_contents_valid() 

154 ) 

155 

156 def total_score(self) -> int: 

157 return ( 

158 self.sum_fields(self.SCORED_FIELDS) - 

159 self.count_where(self.SCORED_FIELDS, [self.FREQ_DAILY_2_WEEKS]) 

160 ) 

161 

162 def get_depression_category(self) -> int: 

163 

164 if not self.has_depression_risk(): 

165 return self.CAT_SUB 

166 

167 q_group_anhedonia = [8, 10] 

168 q_group_dysphoria = [2, 4, 6] 

169 other_q_groups = { 

170 'appetite': [1, 18], 

171 'sleep': [5, 11, 19], 

172 'thinking': [3, 20], 

173 'guilt': [9, 17], 

174 'tired': [7, 16], 

175 'movement': [12, 13], 

176 'suicidal': [14, 15] 

177 } 

178 

179 # Dysphoria or anhedonia must be present at frequency FREQ_DAILY_2_WEEKS 

180 anhedonia_criterion = ( 

181 self.fulfils_group_criteria(q_group_anhedonia, True) or 

182 self.fulfils_group_criteria(q_group_dysphoria, True) 

183 ) 

184 if anhedonia_criterion: 

185 category_count_high_freq = 0 

186 category_count_lower_freq = 0 

187 for qgroup in other_q_groups.values(): 

188 if self.fulfils_group_criteria(qgroup, True): 

189 # Category contains an answer == FREQ_DAILY_2_WEEKS 

190 category_count_high_freq += 1 

191 if self.fulfils_group_criteria(qgroup, False): 

192 # Category contains an answer == FREQ_DAILY_2_WEEKS or 

193 # FREQ_5_7_DAYS_LAST_WEEK 

194 category_count_lower_freq += 1 

195 

196 if category_count_high_freq >= self.MAJOR_THRESH: 

197 # Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS) 

198 # plus 4 other symptom groups at FREQ_DAILY_2_WEEKS 

199 return self.CAT_MAJOR 

200 if category_count_lower_freq >= self.PROB_MAJOR_THRESH: 

201 # Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS) 

202 # plus 3 other symptom groups at FREQ_DAILY_2_WEEKS or 

203 # FREQ_5_7_DAYS_LAST_WEEK 

204 return self.CAT_PROB_MAJOR 

205 if category_count_lower_freq >= self.POSS_MAJOR_THRESH: 

206 # Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS) 

207 # plus 2 other symptom groups at FREQ_DAILY_2_WEEKS or 

208 # FREQ_5_7_DAYS_LAST_WEEK 

209 return self.CAT_POSS_MAJOR 

210 

211 if self.has_depression_risk(): 

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

213 return self.CAT_SUB 

214 

215 return self.CAT_NONCLINICAL 

216 

217 def fulfils_group_criteria(self, qnums: List[int], 

218 nearly_every_day_2w: bool) -> bool: 

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

220 if nearly_every_day_2w: 

221 possible_values = [self.FREQ_DAILY_2_WEEKS] 

222 else: 

223 possible_values = [self.FREQ_5_7_DAYS_LAST_WEEK, 

224 self.FREQ_DAILY_2_WEEKS] 

225 count = self.count_where(qstrings, possible_values) 

226 return count > 0 

227 

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

229 line_step = 20 

230 threshold_line = self.DEPRESSION_RISK_THRESHOLD - 0.5 

231 # noinspection PyTypeChecker 

232 return [TrackerInfo( 

233 value=self.total_score(), 

234 plot_label="CESD-R total score", 

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

236 axis_min=self.MIN_SCORE - 0.5, 

237 axis_max=self.MAX_SCORE + 0.5, 

238 axis_ticks=regular_tracker_axis_ticks_int( 

239 self.MIN_SCORE, 

240 self.MAX_SCORE, 

241 step=line_step 

242 ), 

243 horizontal_lines=equally_spaced_int( 

244 self.MIN_SCORE + line_step, 

245 self.MAX_SCORE - line_step, 

246 step=line_step 

247 ) + [threshold_line], 

248 horizontal_labels=[ 

249 TrackerLabel(threshold_line, 

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

251 ] 

252 )] 

253 

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

255 if not self.is_complete(): 

256 return CTV_INCOMPLETE 

257 return [CtvInfo( 

258 content=f"CESD-R total score {self.total_score()}" 

259 )] 

260 

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

262 return self.standard_task_summary_fields() + [ 

263 SummaryElement( 

264 name="depression_risk", 

265 coltype=Boolean(), 

266 value=self.has_depression_risk(), 

267 comment="Has depression or at risk of depression"), 

268 ] 

269 

270 def has_depression_risk(self) -> bool: 

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

272 

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

274 score = self.total_score() 

275 answer_dict = {None: None} 

276 for option in range(self.N_ANSWERS): 

277 answer_dict[option] = str(option) + " – " + \ 

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

279 q_a = "" 

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

281 q_a += tr_qa( 

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

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

284 ) 

285 

286 tr_total_score = tr_qa( 

287 f"{req.sstring(SS.TOTAL_SCORE)} (0–60)", 

288 score 

289 ) 

290 tr_depression_or_risk_of = tr_qa( 

291 self.wxstring(req, "depression_or_risk_of") + 

292 "? <sup>[1]</sup>", 

293 get_yes_no(req, self.has_depression_risk()) 

294 ) 

295 tr_provisional_diagnosis = tr( 

296 'Provisional diagnosis <sup>[2]</sup>', 

297 self.wxstring(req, 

298 "category_" + str(self.get_depression_category())) 

299 ) 

300 return f""" 

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

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

303 {self.get_is_complete_tr(req)} 

304 {tr_total_score} 

305 {tr_depression_or_risk_of} 

306 {tr_provisional_diagnosis} 

307 </table> 

308 </div> 

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

310 <tr> 

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

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

313 </tr> 

314 {q_a} 

315 </table> 

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

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

318 score &ge; 16 

319 [2] Diagnostic criteria described at 

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

321 </div> 

322 """ # noqa