Coverage for tasks/cesd.py: 59%

75 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/cesd.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- By Joe Kearney, Rudolf Cardinal. 

29 

30""" 

31 

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

33 

34from cardinal_pythonlib.classes import classproperty 

35from cardinal_pythonlib.stringfunc import strseq 

36from semantic_version import Version 

37from sqlalchemy.ext.declarative import DeclarativeMeta 

38from sqlalchemy.sql.sqltypes import Boolean 

39 

40from camcops_server.cc_modules.cc_constants import CssClass 

41from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE 

42from camcops_server.cc_modules.cc_db import add_multiple_columns 

43from camcops_server.cc_modules.cc_html import get_yes_no, tr_qa 

44from camcops_server.cc_modules.cc_request import CamcopsRequest 

45 

46from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

47from camcops_server.cc_modules.cc_task import ( 

48 get_from_dict, 

49 Task, 

50 TaskHasPatientMixin, 

51) 

52from camcops_server.cc_modules.cc_text import SS 

53from camcops_server.cc_modules.cc_trackerhelpers import ( 

54 equally_spaced_int, 

55 regular_tracker_axis_ticks_int, 

56 TrackerInfo, 

57 TrackerLabel, 

58) 

59 

60 

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

62# CESD 

63# ============================================================================= 

64 

65 

66class CesdMetaclass(DeclarativeMeta): 

67 """ 

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

69 """ 

70 

71 # noinspection PyInitNewSignature 

72 def __init__( 

73 cls: Type["Cesd"], 

74 name: str, 

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

76 classdict: Dict[str, Any], 

77 ) -> None: 

78 add_multiple_columns( 

79 cls, 

80 "q", 

81 1, 

82 cls.N_QUESTIONS, 

83 minimum=0, 

84 maximum=4, 

85 comment_fmt=( 

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

87 ), 

88 comment_strings=[ 

89 "sensitivity/irritability", 

90 "poor appetite", 

91 "unshakeable blues", 

92 "low self-esteem", 

93 "poor concentration", 

94 "depressed", 

95 "everything effortful", 

96 "hopeful", 

97 "feelings of failure", 

98 "fearful", 

99 "sleep restless", 

100 "happy", 

101 "uncommunicative", 

102 "lonely", 

103 "perceived unfriendliness", 

104 "enjoyment", 

105 "crying spells", 

106 "sadness", 

107 "feeling disliked", 

108 "could not get going", 

109 ], 

110 ) 

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

112 

113 

114class Cesd(TaskHasPatientMixin, Task, metaclass=CesdMetaclass): 

115 """ 

116 Server implementation of the CESD task. 

117 """ 

118 

119 __tablename__ = "cesd" 

120 shortname = "CESD" 

121 provides_trackers = True 

122 extrastring_taskname = "cesd" 

123 N_QUESTIONS = 20 

124 N_ANSWERS = 4 

125 DEPRESSION_RISK_THRESHOLD = 16 

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

127 TASK_FIELDS = SCORED_FIELDS 

128 MIN_SCORE = 0 

129 MAX_SCORE = 3 * N_QUESTIONS 

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

131 

132 @staticmethod 

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

134 _ = req.gettext 

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

136 

137 # noinspection PyMethodParameters 

138 @classproperty 

139 def minimum_client_version(cls) -> Version: 

140 return Version("2.2.8") 

141 

142 def is_complete(self) -> bool: 

143 return ( 

144 self.all_fields_not_none(self.TASK_FIELDS) 

145 and self.field_contents_valid() 

146 ) 

147 

148 def total_score(self) -> int: 

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

150 total = 0 

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

152 score = getattr(self, fieldname) 

153 if score is None: 

154 continue 

155 if qnum in self.REVERSE_SCORED_QUESTIONS: 

156 total += 3 - score 

157 else: 

158 total += score 

159 return total 

160 

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

162 line_step = 20 

163 threshold_line = self.DEPRESSION_RISK_THRESHOLD - 0.5 

164 # noinspection PyTypeChecker 

165 return [ 

166 TrackerInfo( 

167 value=self.total_score(), 

168 plot_label="CESD total score", 

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

170 axis_min=self.MIN_SCORE - 0.5, 

171 axis_max=self.MAX_SCORE + 0.5, 

172 axis_ticks=regular_tracker_axis_ticks_int( 

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

174 ), 

175 horizontal_lines=equally_spaced_int( 

176 self.MIN_SCORE + line_step, 

177 self.MAX_SCORE - line_step, 

178 step=line_step, 

179 ) 

180 + [threshold_line], 

181 horizontal_labels=[ 

182 TrackerLabel( 

183 threshold_line, 

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

185 ) 

186 ], 

187 ) 

188 ] 

189 

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

191 if not self.is_complete(): 

192 return CTV_INCOMPLETE 

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

194 

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

196 return self.standard_task_summary_fields() + [ 

197 SummaryElement( 

198 name="depression_risk", 

199 coltype=Boolean(), 

200 value=self.has_depression_risk(), 

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

202 ) 

203 ] 

204 

205 def has_depression_risk(self) -> bool: 

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

207 

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

209 score = self.total_score() 

210 answer_dict = {None: None} 

211 for option in range(self.N_ANSWERS): 

212 answer_dict[option] = ( 

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

214 ) 

215 q_a = "" 

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

217 q_a += tr_qa( 

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

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

220 ) 

221 

222 tr_total_score = ( 

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

224 ) 

225 tr_depression_or_risk_of = ( 

226 tr_qa( 

227 self.wxstring(req, "depression_or_risk_of") 

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

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

230 ), 

231 ) 

232 return f""" 

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

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

235 {self.get_is_complete_tr(req)} 

236 {tr_total_score} 

237 {tr_depression_or_risk_of} 

238 </table> 

239 </div> 

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

241 <tr> 

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

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

244 </tr> 

245 {q_a} 

246 </table> 

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

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

249 score &ge; 16 

250 </div> 

251 """