Coverage for tasks/hama.py: 52%

1"""

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

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

29

30 from cardinal_pythonlib.stringfunc import strseq

31 from sqlalchemy.sql.sqltypes import Integer

32

33 from camcops_server.cc_modules.cc_constants import CssClass

34 from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE

35 from camcops_server.cc_modules.cc_db import add_multiple_columns

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

37 from camcops_server.cc_modules.cc_request import CamcopsRequest

38 from camcops_server.cc_modules.cc_sqla_coltypes import SummaryCategoryColType

39 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

40 from camcops_server.cc_modules.cc_task import (

41 get_from_dict,

42 Task,

43 TaskHasClinicianMixin,

44 TaskHasPatientMixin,

45)

46 from camcops_server.cc_modules.cc_text import SS

47 from camcops_server.cc_modules.cc_trackerhelpers import (

48 TrackerInfo,

49 TrackerLabel,

50)

51

52

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

54# HAM-A

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

56

57

58 class 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 )