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

28 

29""" 

30 

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

32 

33from cardinal_pythonlib.classes import classproperty 

34from cardinal_pythonlib.stringfunc import strseq 

35from semantic_version import Version 

36from sqlalchemy.ext.declarative import DeclarativeMeta 

37from sqlalchemy.sql.sqltypes import Boolean 

38 

39from camcops_server.cc_modules.cc_constants import CssClass 

40from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE 

41from camcops_server.cc_modules.cc_db import add_multiple_columns 

42from camcops_server.cc_modules.cc_html import get_yes_no, tr_qa 

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 

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

63 

64class CesdMetaclass(DeclarativeMeta): 

65 """ 

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

67 """ 

68 # noinspection PyInitNewSignature 

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

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=( 

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

78 ), 

79 comment_strings=[ 

80 "sensitivity/irritability", 

81 "poor appetite", 

82 "unshakeable blues", 

83 "low self-esteem", 

84 "poor concentration", 

85 "depressed", 

86 "everything effortful", 

87 "hopeful", 

88 "feelings of failure", 

89 "fearful", 

90 "sleep restless", 

91 "happy", 

92 "uncommunicative", 

93 "lonely", 

94 "perceived unfriendliness", 

95 "enjoyment", 

96 "crying spells", 

97 "sadness", 

98 "feeling disliked", 

99 "could not get going", 

100 ] 

101 ) 

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

103 

104 

105class Cesd(TaskHasPatientMixin, Task, 

106 metaclass=CesdMetaclass): 

107 """ 

108 Server implementation of the CESD task. 

109 """ 

110 __tablename__ = 'cesd' 

111 shortname = 'CESD' 

112 provides_trackers = True 

113 extrastring_taskname = "cesd" 

114 N_QUESTIONS = 20 

115 N_ANSWERS = 4 

116 DEPRESSION_RISK_THRESHOLD = 16 

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

118 TASK_FIELDS = SCORED_FIELDS 

119 MIN_SCORE = 0 

120 MAX_SCORE = 3 * N_QUESTIONS 

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

122 

123 @staticmethod 

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

125 _ = req.gettext 

126 return _('Center for Epidemiologic Studies Depression Scale') 

127 

128 # noinspection PyMethodParameters 

129 @classproperty 

130 def minimum_client_version(cls) -> Version: 

131 return Version("2.2.8") 

132 

133 def is_complete(self) -> bool: 

134 return ( 

135 self.all_fields_not_none(self.TASK_FIELDS) and 

136 self.field_contents_valid() 

137 ) 

138 

139 def total_score(self) -> int: 

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

141 total = 0 

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

143 score = getattr(self, fieldname) 

144 if score is None: 

145 continue 

146 if qnum in self.REVERSE_SCORED_QUESTIONS: 

147 total += 3 - score 

148 else: 

149 total += score 

150 return total 

151 

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

153 line_step = 20 

154 threshold_line = self.DEPRESSION_RISK_THRESHOLD - 0.5 

155 # noinspection PyTypeChecker 

156 return [TrackerInfo( 

157 value=self.total_score(), 

158 plot_label="CESD total score", 

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

160 axis_min=self.MIN_SCORE - 0.5, 

161 axis_max=self.MAX_SCORE + 0.5, 

162 axis_ticks=regular_tracker_axis_ticks_int( 

163 self.MIN_SCORE, 

164 self.MAX_SCORE, 

165 step=line_step 

166 ), 

167 horizontal_lines=equally_spaced_int( 

168 self.MIN_SCORE + line_step, 

169 self.MAX_SCORE - line_step, 

170 step=line_step 

171 ) + [threshold_line], 

172 horizontal_labels=[ 

173 TrackerLabel(threshold_line, 

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

175 ] 

176 )] 

177 

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

179 if not self.is_complete(): 

180 return CTV_INCOMPLETE 

181 return [CtvInfo( 

182 content=f"CESD total score {self.total_score()}" 

183 )] 

184 

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

186 return self.standard_task_summary_fields() + [ 

187 SummaryElement( 

188 name="depression_risk", 

189 coltype=Boolean(), 

190 value=self.has_depression_risk(), 

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

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 = {None: None} 

200 for option in range(self.N_ANSWERS): 

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

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

203 q_a = "" 

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

205 q_a += tr_qa( 

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

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

208 ) 

209 

210 tr_total_score = tr_qa( 

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

212 score 

213 ), 

214 tr_depression_or_risk_of = tr_qa( 

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

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

217 get_yes_no(req, self.has_depression_risk()) 

218 ), 

219 return f""" 

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

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

222 {self.get_is_complete_tr(req)} 

223 {tr_total_score} 

224 {tr_depression_or_risk_of} 

225 </table> 

226 </div> 

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

228 <tr> 

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

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

231 </tr> 

232 {q_a} 

233 </table> 

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

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

236 score &ge; 16 

237 </div> 

238 """