Coverage for tasks/hamd7.py: 53%

66 statements  

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

1""" 

2camcops_server/tasks/hamd7.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""" 

27 

28from typing import Any, cast, List, Optional, Type 

29 

30from cardinal_pythonlib.stringfunc import strseq 

31from sqlalchemy.sql.sqltypes import Integer 

32 

33from camcops_server.cc_modules.cc_constants import CssClass 

34from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

35from camcops_server.cc_modules.cc_db import add_multiple_columns 

36from camcops_server.cc_modules.cc_html import answer, tr, tr_qa 

37from camcops_server.cc_modules.cc_request import CamcopsRequest 

38from camcops_server.cc_modules.cc_sqla_coltypes import ( 

39 SummaryCategoryColType, 

40) 

41from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

42from camcops_server.cc_modules.cc_task import ( 

43 get_from_dict, 

44 Task, 

45 TaskHasClinicianMixin, 

46 TaskHasPatientMixin, 

47) 

48from camcops_server.cc_modules.cc_text import SS 

49from camcops_server.cc_modules.cc_trackerhelpers import ( 

50 TrackerInfo, 

51 TrackerLabel, 

52) 

53 

54 

55# ============================================================================= 

56# HAMD-7 

57# ============================================================================= 

58 

59 

60class Hamd7( # type: ignore[misc] 

61 TaskHasPatientMixin, 

62 TaskHasClinicianMixin, 

63 Task, 

64): 

65 """ 

66 Server implementation of the HAMD-7 task. 

67 """ 

68 

69 __tablename__ = "hamd7" 

70 shortname = "HAMD-7" 

71 info_filename_stem = "hamd" 

72 provides_trackers = True 

73 

74 NQUESTIONS = 7 

75 

76 @classmethod 

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

78 add_multiple_columns( 

79 cls, 

80 "q", 

81 1, 

82 5, 

83 minimum=0, 

84 maximum=4, 

85 comment_fmt="Q{n}, {s} (0-4, higher worse)", 

86 comment_strings=[ 

87 "depressed mood", 

88 "guilt", 

89 "interest/pleasure/level of activities", 

90 "psychological anxiety", 

91 "somatic anxiety", 

92 ], 

93 ) 

94 add_multiple_columns( 

95 cls, 

96 "q", 

97 6, 

98 6, 

99 minimum=0, 

100 maximum=2, 

101 comment_fmt="Q{n}, {s} (0-2, higher worse)", 

102 comment_strings=[ 

103 "energy/somatic symptoms", 

104 ], 

105 ) 

106 add_multiple_columns( 

107 cls, 

108 "q", 

109 7, 

110 7, 

111 minimum=0, 

112 maximum=4, 

113 comment_fmt="Q{n}, {s} (0-4, higher worse)", 

114 comment_strings=[ 

115 "suicide", 

116 ], 

117 ) 

118 

119 TASK_FIELDS = strseq("q", 1, NQUESTIONS) 

120 MAX_SCORE = 26 

121 

122 @staticmethod 

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

124 _ = req.gettext 

125 return _("Hamilton Rating Scale for Depression (7-item scale)") 

126 

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

128 return [ 

129 TrackerInfo( 

130 value=self.total_score(), 

131 plot_label="HAM-D-7 total score", 

132 axis_label=f"Total score (out of {self.MAX_SCORE})", 

133 axis_min=-0.5, 

134 axis_max=self.MAX_SCORE + 0.5, 

135 horizontal_lines=[19.5, 11.5, 3.5], 

136 horizontal_labels=[ 

137 TrackerLabel(23, self.wxstring(req, "severity_severe")), 

138 TrackerLabel( 

139 15.5, self.wxstring(req, "severity_moderate") 

140 ), 

141 TrackerLabel(7.5, self.wxstring(req, "severity_mild")), 

142 TrackerLabel(1.75, self.wxstring(req, "severity_none")), 

143 ], 

144 ) 

145 ] 

146 

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

148 if not self.is_complete(): 

149 return CTV_INCOMPLETE 

150 return [ 

151 CtvInfo( 

152 content=( 

153 f"HAM-D-7 total score " 

154 f"{self.total_score()}/{self.MAX_SCORE} " 

155 f"({self.severity(req)})" 

156 ) 

157 ) 

158 ] 

159 

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

161 return self.standard_task_summary_fields() + [ 

162 SummaryElement( 

163 name="total", 

164 coltype=Integer(), 

165 value=self.total_score(), 

166 comment=f"Total score (/{self.MAX_SCORE})", 

167 ), 

168 SummaryElement( 

169 name="severity", 

170 coltype=SummaryCategoryColType, 

171 value=self.severity(req), 

172 comment="Severity", 

173 ), 

174 ] 

175 

176 def is_complete(self) -> bool: 

177 return ( 

178 self.all_fields_not_none(self.TASK_FIELDS) 

179 and self.field_contents_valid() 

180 ) 

181 

182 def total_score(self) -> int: 

183 return cast(int, self.sum_fields(self.TASK_FIELDS)) 

184 

185 def severity(self, req: CamcopsRequest) -> str: 

186 score = self.total_score() 

187 if score >= 20: 

188 return self.wxstring(req, "severity_severe") 

189 elif score >= 12: 

190 return self.wxstring(req, "severity_moderate") 

191 elif score >= 4: 

192 return self.wxstring(req, "severity_mild") 

193 else: 

194 return self.wxstring(req, "severity_none") 

195 

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

197 score = self.total_score() 

198 severity = self.severity(req) 

199 answer_dicts = [] 

200 for q in range(1, self.NQUESTIONS + 1): 

201 d: dict[Optional[int], Optional[str]] = {None: None} 

202 for option in range(0, 5): 

203 if q == 6 and option > 2: 

204 continue 

205 d[option] = self.wxstring( 

206 req, "q" + str(q) + "_option" + str(option) 

207 ) 

208 answer_dicts.append(d) 

209 

210 q_a = "" 

211 for q in range(1, self.NQUESTIONS + 1): 

212 q_a += tr_qa( 

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

214 get_from_dict( 

215 answer_dicts[q - 1], getattr(self, "q" + str(q)) 

216 ), 

217 ) 

218 

219 return """ 

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

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

222 {tr_is_complete} 

223 {total_score} 

224 {severity} 

225 </table> 

226 </div> 

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

228 <tr> 

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

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

231 </tr> 

232 {q_a} 

233 </table> 

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

235 [1] ≥20 severe, ≥12 moderate, ≥4 mild, &lt;4 none. 

236 </div> 

237 """.format( 

238 CssClass=CssClass, 

239 tr_is_complete=self.get_is_complete_tr(req), 

240 total_score=tr( 

241 req.sstring(SS.TOTAL_SCORE), 

242 answer(score) + " / {}".format(self.MAX_SCORE), 

243 ), 

244 severity=tr_qa( 

245 self.wxstring(req, "severity") + " <sup>[1]</sup>", severity 

246 ), 

247 q_a=q_a, 

248 )