Coverage for tasks/hamd7.py: 54%

68 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/hamd7.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CamCOPS. 

12 

13 CamCOPS is free software: you can redistribute it and/or modify 

14 it under the terms of the GNU General Public License as published by 

15 the Free Software Foundation, either version 3 of the License, or 

16 (at your option) any later version. 

17 

18 CamCOPS is distributed in the hope that it will be useful, 

19 but WITHOUT ANY WARRANTY; without even the implied warranty of 

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

23 You should have received a copy of the GNU General Public License 

24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

25 

26=============================================================================== 

27 

28""" 

29 

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

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.sqltypes import Integer 

35 

36from camcops_server.cc_modules.cc_constants import CssClass 

37from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

38from camcops_server.cc_modules.cc_db import add_multiple_columns 

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

40from camcops_server.cc_modules.cc_request import CamcopsRequest 

41from camcops_server.cc_modules.cc_sqla_coltypes import ( 

42 SummaryCategoryColType, 

43 ZERO_TO_TWO_CHECKER, 

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 TaskHasClinicianMixin, 

50 TaskHasPatientMixin, 

51) 

52from camcops_server.cc_modules.cc_text import SS 

53from camcops_server.cc_modules.cc_trackerhelpers import ( 

54 TrackerInfo, 

55 TrackerLabel, 

56) 

57 

58 

59# ============================================================================= 

60# HAMD-7 

61# ============================================================================= 

62 

63 

64class Hamd7Metaclass(DeclarativeMeta): 

65 # noinspection PyInitNewSignature,PyUnresolvedReferences 

66 def __init__( 

67 cls: Type["Hamd7"], 

68 name: str, 

69 bases: Tuple[Type, ...], 

70 classdict: Dict[str, Any], 

71 ) -> None: 

72 add_multiple_columns( 

73 cls, 

74 "q", 

75 1, 

76 cls.NQUESTIONS, 

77 minimum=0, 

78 maximum=4, # see below 

79 comment_fmt="Q{n}, {s} (0-4, except Q6 0-2; higher worse)", 

80 comment_strings=[ 

81 "depressed mood", 

82 "guilt", 

83 "interest/pleasure/level of activities", 

84 "psychological anxiety", 

85 "somatic anxiety", 

86 "energy/somatic symptoms", 

87 "suicide", 

88 ], 

89 ) 

90 # Now fix the wrong bits. Hardly elegant! 

91 cls.q6.set_permitted_value_checker(ZERO_TO_TWO_CHECKER) 

92 

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

94 

95 

96class Hamd7( 

97 TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=Hamd7Metaclass 

98): 

99 """ 

100 Server implementation of the HAMD-7 task. 

101 """ 

102 

103 __tablename__ = "hamd7" 

104 shortname = "HAMD-7" 

105 info_filename_stem = "hamd" 

106 provides_trackers = True 

107 

108 NQUESTIONS = 7 

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

110 MAX_SCORE = 26 

111 

112 @staticmethod 

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

114 _ = req.gettext 

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

116 

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

118 return [ 

119 TrackerInfo( 

120 value=self.total_score(), 

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

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

123 axis_min=-0.5, 

124 axis_max=self.MAX_SCORE + 0.5, 

125 horizontal_lines=[19.5, 11.5, 3.5], 

126 horizontal_labels=[ 

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

128 TrackerLabel( 

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

130 ), 

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

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

133 ], 

134 ) 

135 ] 

136 

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

138 if not self.is_complete(): 

139 return CTV_INCOMPLETE 

140 return [ 

141 CtvInfo( 

142 content=( 

143 f"HAM-D-7 total score " 

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

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

146 ) 

147 ) 

148 ] 

149 

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

151 return self.standard_task_summary_fields() + [ 

152 SummaryElement( 

153 name="total", 

154 coltype=Integer(), 

155 value=self.total_score(), 

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

157 ), 

158 SummaryElement( 

159 name="severity", 

160 coltype=SummaryCategoryColType, 

161 value=self.severity(req), 

162 comment="Severity", 

163 ), 

164 ] 

165 

166 def is_complete(self) -> bool: 

167 return ( 

168 self.all_fields_not_none(self.TASK_FIELDS) 

169 and self.field_contents_valid() 

170 ) 

171 

172 def total_score(self) -> int: 

173 return self.sum_fields(self.TASK_FIELDS) 

174 

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

176 score = self.total_score() 

177 if score >= 20: 

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

179 elif score >= 12: 

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

181 elif score >= 4: 

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

183 else: 

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

185 

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

187 score = self.total_score() 

188 severity = self.severity(req) 

189 answer_dicts = [] 

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

191 d = {None: None} 

192 for option in range(0, 5): 

193 if q == 6 and option > 2: 

194 continue 

195 d[option] = self.wxstring( 

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

197 ) 

198 answer_dicts.append(d) 

199 

200 q_a = "" 

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

202 q_a += tr_qa( 

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

204 get_from_dict( 

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

206 ), 

207 ) 

208 

209 return """ 

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

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

212 {tr_is_complete} 

213 {total_score} 

214 {severity} 

215 </table> 

216 </div> 

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

218 <tr> 

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

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

221 </tr> 

222 {q_a} 

223 </table> 

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

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

226 </div> 

227 """.format( 

228 CssClass=CssClass, 

229 tr_is_complete=self.get_is_complete_tr(req), 

230 total_score=tr( 

231 req.sstring(SS.TOTAL_SCORE), 

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

233 ), 

234 severity=tr_qa( 

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

236 ), 

237 q_a=q_a, 

238 )