Coverage for tasks/hama.py: 52%

61 statements  

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

1""" 

2camcops_server/tasks/hama.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 CtvInfo, CTV_INCOMPLETE 

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 SummaryCategoryColType 

39from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

40from camcops_server.cc_modules.cc_task import ( 

41 get_from_dict, 

42 Task, 

43 TaskHasClinicianMixin, 

44 TaskHasPatientMixin, 

45) 

46from camcops_server.cc_modules.cc_text import SS 

47from camcops_server.cc_modules.cc_trackerhelpers import ( 

48 TrackerInfo, 

49 TrackerLabel, 

50) 

51 

52 

53# ============================================================================= 

54# HAM-A 

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

56 

57 

58class Hama( # type: ignore[misc] 

59 TaskHasPatientMixin, 

60 TaskHasClinicianMixin, 

61 Task, 

62): 

63 """ 

64 Server implementation of the HAM-A task. 

65 """ 

66 

67 __tablename__ = "hama" 

68 shortname = "HAM-A" 

69 provides_trackers = True 

70 

71 NQUESTIONS = 14 

72 

73 @classmethod 

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

75 add_multiple_columns( 

76 cls, 

77 "q", 

78 1, 

79 cls.NQUESTIONS, 

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

81 minimum=0, 

82 maximum=4, 

83 comment_strings=[ 

84 "anxious mood", 

85 "tension", 

86 "fears", 

87 "insomnia", 

88 "concentration/memory", 

89 "depressed mood", 

90 "somatic, muscular", 

91 "somatic, sensory", 

92 "cardiovascular", 

93 "respiratory", 

94 "gastrointestinal", 

95 "genitourinary", 

96 "other autonomic", 

97 "behaviour in interview", 

98 ], 

99 ) 

100 

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

102 MAX_SCORE = 56 

103 

104 @staticmethod 

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

106 _ = req.gettext 

107 return _("Hamilton Rating Scale for Anxiety") 

108 

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

110 return [ 

111 TrackerInfo( 

112 value=self.total_score(), 

113 plot_label="HAM-A total score", 

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

115 axis_min=-0.5, 

116 axis_max=self.MAX_SCORE + 0.5, 

117 horizontal_lines=[30.5, 24.5, 17.5], 

118 horizontal_labels=[ 

119 TrackerLabel(33, req.sstring(SS.VERY_SEVERE)), 

120 TrackerLabel(27.5, req.sstring(SS.MODERATE_TO_SEVERE)), 

121 TrackerLabel(21, req.sstring(SS.MILD_TO_MODERATE)), 

122 TrackerLabel(8.75, req.sstring(SS.MILD)), 

123 ], 

124 ) 

125 ] 

126 

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

128 if not self.is_complete(): 

129 return CTV_INCOMPLETE 

130 return [ 

131 CtvInfo( 

132 content=( 

133 f"HAM-A total score {self.total_score()}/{self.MAX_SCORE} " 

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

135 ) 

136 ) 

137 ] 

138 

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

140 return self.standard_task_summary_fields() + [ 

141 SummaryElement( 

142 name="total", 

143 coltype=Integer(), 

144 value=self.total_score(), 

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

146 ), 

147 SummaryElement( 

148 name="severity", 

149 coltype=SummaryCategoryColType, 

150 value=self.severity(req), 

151 comment="Severity", 

152 ), 

153 ] 

154 

155 def is_complete(self) -> bool: 

156 return ( 

157 self.all_fields_not_none(self.TASK_FIELDS) 

158 and self.field_contents_valid() 

159 ) 

160 

161 def total_score(self) -> int: 

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

163 

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

165 score = self.total_score() 

166 if score >= 31: 

167 return req.sstring(SS.VERY_SEVERE) 

168 elif score >= 25: 

169 return req.sstring(SS.MODERATE_TO_SEVERE) 

170 elif score >= 18: 

171 return req.sstring(SS.MILD_TO_MODERATE) 

172 else: 

173 return req.sstring(SS.MILD) 

174 

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

176 score = self.total_score() 

177 severity = self.severity(req) 

178 answer_dicts = [] 

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

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

181 for option in range(0, 4 + 1): 

182 d[option] = self.wxstring( 

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

184 ) 

185 answer_dicts.append(d) 

186 q_a = "" 

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

188 q_a += tr_qa( 

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

190 + " " 

191 + self.wxstring(req, "q" + str(q) + "_question"), 

192 get_from_dict( 

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

194 ), 

195 ) 

196 return """ 

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

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

199 {tr_is_complete} 

200 {total_score} 

201 {symptom_severity} 

202 </table> 

203 </div> 

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

205 <tr> 

206 <th width="50%">Question</th> 

207 <th width="50%">Answer</th> 

208 </tr> 

209 {q_a} 

210 </table> 

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

212 [1] ≥31 very severe, ≥25 moderate to severe, 

213 ≥18 mild to moderate, otherwise mild. 

214 </div> 

215 """.format( 

216 CssClass=CssClass, 

217 tr_is_complete=self.get_is_complete_tr(req), 

218 total_score=tr( 

219 req.sstring(SS.TOTAL_SCORE), 

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

221 ), 

222 symptom_severity=tr_qa( 

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

224 severity, 

225 ), 

226 q_a=q_a, 

227 )