Coverage for tasks/cesd.py: 58%

72 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 15:51 +0100

1""" 

2camcops_server/tasks/cesd.py 

3 

4=============================================================================== 

5 

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

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

8 

9 This file is part of CamCOPS. 

10 

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

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

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

14 (at your option) any later version. 

15 

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

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

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

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

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

23 

24=============================================================================== 

25 

26- By Joe Kearney, Rudolf Cardinal. 

27 

28""" 

29 

30from typing import Any, 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 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 get_yes_no, tr_qa 

41from camcops_server.cc_modules.cc_request import CamcopsRequest 

42 

43from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

44from camcops_server.cc_modules.cc_task import ( 

45 get_from_dict, 

46 Task, 

47 TaskHasPatientMixin, 

48) 

49from camcops_server.cc_modules.cc_text import SS 

50from camcops_server.cc_modules.cc_trackerhelpers import ( 

51 equally_spaced_int, 

52 regular_tracker_axis_ticks_int, 

53 TrackerInfo, 

54 TrackerLabel, 

55) 

56 

57 

58# ============================================================================= 

59# CESD 

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

61 

62 

63class Cesd( # type: ignore[misc] 

64 TaskHasPatientMixin, 

65 Task, 

66): 

67 """ 

68 Server implementation of the CESD task. 

69 """ 

70 

71 __tablename__ = "cesd" 

72 shortname = "CESD" 

73 provides_trackers = True 

74 extrastring_taskname = "cesd" 

75 N_QUESTIONS = 20 

76 N_ANSWERS = 4 

77 DEPRESSION_RISK_THRESHOLD = 16 

78 

79 @classmethod 

80 def extend_columns(cls: Type["Cesd"], **kwargs: Any) -> None: 

81 add_multiple_columns( 

82 cls, 

83 "q", 

84 1, 

85 cls.N_QUESTIONS, 

86 minimum=0, 

87 maximum=4, 

88 comment_fmt=( 

89 "Q{n} ({s}) (0 rarely/none of the time - 4 all of the time)" 

90 ), 

91 comment_strings=[ 

92 "sensitivity/irritability", 

93 "poor appetite", 

94 "unshakeable blues", 

95 "low self-esteem", 

96 "poor concentration", 

97 "depressed", 

98 "everything effortful", 

99 "hopeful", 

100 "feelings of failure", 

101 "fearful", 

102 "sleep restless", 

103 "happy", 

104 "uncommunicative", 

105 "lonely", 

106 "perceived unfriendliness", 

107 "enjoyment", 

108 "crying spells", 

109 "sadness", 

110 "feeling disliked", 

111 "could not get going", 

112 ], 

113 ) 

114 

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

116 TASK_FIELDS = SCORED_FIELDS 

117 MIN_SCORE = 0 

118 MAX_SCORE = 3 * N_QUESTIONS 

119 REVERSE_SCORED_QUESTIONS = [4, 8, 12, 16] 

120 

121 @staticmethod 

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

123 _ = req.gettext 

124 return _("Center for Epidemiologic Studies Depression Scale") 

125 

126 # noinspection PyMethodParameters 

127 @classproperty 

128 def minimum_client_version(cls) -> Version: 

129 return Version("2.2.8") 

130 

131 def is_complete(self) -> bool: 

132 return ( 

133 self.all_fields_not_none(self.TASK_FIELDS) 

134 and self.field_contents_valid() 

135 ) 

136 

137 def total_score(self) -> int: 

138 # Need to store values as per original then flip here 

139 total = 0 

140 for qnum, fieldname in enumerate(self.SCORED_FIELDS, start=1): 

141 score = getattr(self, fieldname) 

142 if score is None: 

143 continue 

144 if qnum in self.REVERSE_SCORED_QUESTIONS: 

145 total += 3 - score 

146 else: 

147 total += score 

148 return total 

149 

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

151 line_step = 20 

152 threshold_line = self.DEPRESSION_RISK_THRESHOLD - 0.5 

153 # noinspection PyTypeChecker 

154 return [ 

155 TrackerInfo( 

156 value=self.total_score(), 

157 plot_label="CESD total score", 

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

159 axis_min=self.MIN_SCORE - 0.5, 

160 axis_max=self.MAX_SCORE + 0.5, 

161 axis_ticks=regular_tracker_axis_ticks_int( 

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

163 ), 

164 horizontal_lines=equally_spaced_int( 

165 self.MIN_SCORE + line_step, 

166 self.MAX_SCORE - line_step, 

167 step=line_step, 

168 ) 

169 + [threshold_line], 

170 horizontal_labels=[ 

171 TrackerLabel( 

172 threshold_line, 

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

174 ) 

175 ], 

176 ) 

177 ] 

178 

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

180 if not self.is_complete(): 

181 return CTV_INCOMPLETE 

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

183 

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

185 return self.standard_task_summary_fields() + [ 

186 SummaryElement( 

187 name="depression_risk", 

188 coltype=Boolean(), 

189 value=self.has_depression_risk(), 

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

191 ) 

192 ] 

193 

194 def has_depression_risk(self) -> bool: 

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

196 

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

198 score = self.total_score() 

199 answer_dict: dict[Optional[int], Optional[str]] = {None: None} 

200 for option in range(self.N_ANSWERS): 

201 answer_dict[option] = ( 

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

203 ) 

204 q_a = "" 

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

206 q_a += tr_qa( 

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

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

209 ) 

210 

211 tr_total_score = ( 

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

213 ) 

214 tr_depression_or_risk_of = ( 

215 tr_qa( 

216 self.wxstring(req, "depression_or_risk_of") 

217 + "? <sup>[1]</sup>", 

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

219 ), 

220 ) 

221 return f""" 

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

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

224 {self.get_is_complete_tr(req)} 

225 {tr_total_score} 

226 {tr_depression_or_risk_of} 

227 </table> 

228 </div> 

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

230 <tr> 

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

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

233 </tr> 

234 {q_a} 

235 </table> 

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

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

238 score &ge; 16 

239 </div> 

240 """