Coverage for tasks/cesd.py: 58%

1"""

2 camcops_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

30 from typing import Any, List, Optional, Type

31

32 from cardinal_pythonlib.classes import classproperty

33 from cardinal_pythonlib.stringfunc import strseq

34 from semantic_version import Version

35 from sqlalchemy.sql.sqltypes import Boolean

36

37 from camcops_server.cc_modules.cc_constants import CssClass

38 from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE

39 from camcops_server.cc_modules.cc_db import add_multiple_columns

40 from camcops_server.cc_modules.cc_html import get_yes_no, tr_qa

41 from camcops_server.cc_modules.cc_request import CamcopsRequest

42

43 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

44 from camcops_server.cc_modules.cc_task import (

45 get_from_dict,

46 Task,

47 TaskHasPatientMixin,

48)

49 from camcops_server.cc_modules.cc_text import SS

50 from 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

63 class 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 """