Coverage for tasks/gad7.py: 52%

65 statements  

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

1""" 

2camcops_server/tasks/gad7.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_snomed import SnomedExpression, SnomedLookup 

39from camcops_server.cc_modules.cc_sqla_coltypes import SummaryCategoryColType 

40from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

41from camcops_server.cc_modules.cc_task import ( 

42 get_from_dict, 

43 Task, 

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# GAD-7 

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

56 

57 

58class Gad7( # type: ignore[misc] 

59 TaskHasPatientMixin, 

60 Task, 

61): 

62 """ 

63 Server implementation of the GAD-7 task. 

64 """ 

65 

66 __tablename__ = "gad7" 

67 shortname = "GAD-7" 

68 provides_trackers = True 

69 

70 NQUESTIONS = 7 

71 

72 @classmethod 

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

74 add_multiple_columns( 

75 cls, 

76 "q", 

77 1, 

78 cls.NQUESTIONS, 

79 minimum=0, 

80 maximum=3, 

81 comment_fmt="Q{n}, {s} (0 not at all - 3 nearly every day)", 

82 comment_strings=[ 

83 "nervous/anxious/on edge", 

84 "can't stop/control worrying", 

85 "worrying too much about different things", 

86 "trouble relaxing", 

87 "restless", 

88 "irritable", 

89 "afraid", 

90 ], 

91 ) 

92 

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

94 MAX_SCORE = 21 

95 

96 @staticmethod 

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

98 _ = req.gettext 

99 return _("Generalized Anxiety Disorder Assessment") 

100 

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

102 return [ 

103 TrackerInfo( 

104 value=self.total_score(), 

105 plot_label="GAD-7 total score", 

106 axis_label="Total score (out of 21)", 

107 axis_min=-0.5, 

108 axis_max=self.MAX_SCORE + 0.5, 

109 horizontal_lines=[14.5, 9.5, 4.5], 

110 horizontal_labels=[ 

111 TrackerLabel(17, req.sstring(SS.SEVERE)), 

112 TrackerLabel(12, req.sstring(SS.MODERATE)), 

113 TrackerLabel(7, req.sstring(SS.MILD)), 

114 TrackerLabel(2.25, req.sstring(SS.NONE)), 

115 ], 

116 ) 

117 ] 

118 

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

120 if not self.is_complete(): 

121 return CTV_INCOMPLETE 

122 return [ 

123 CtvInfo( 

124 content=( 

125 f"GAD-7 total score {self.total_score()}/{self.MAX_SCORE} " 

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

127 ) 

128 ) 

129 ] 

130 

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

132 return self.standard_task_summary_fields() + [ 

133 SummaryElement( 

134 name="total", 

135 coltype=Integer(), 

136 value=self.total_score(), 

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

138 ), 

139 SummaryElement( 

140 name="severity", 

141 coltype=SummaryCategoryColType, 

142 value=self.severity(req), 

143 comment="Severity", 

144 ), 

145 ] 

146 

147 def is_complete(self) -> bool: 

148 return ( 

149 self.all_fields_not_none(self.TASK_FIELDS) 

150 and self.field_contents_valid() 

151 ) 

152 

153 def total_score(self) -> int: 

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

155 

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

157 score = self.total_score() 

158 if score >= 15: 

159 severity = req.sstring(SS.SEVERE) 

160 elif score >= 10: 

161 severity = req.sstring(SS.MODERATE) 

162 elif score >= 5: 

163 severity = req.sstring(SS.MILD) 

164 else: 

165 severity = req.sstring(SS.NONE) 

166 return severity 

167 

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

169 score = self.total_score() 

170 severity = self.severity(req) 

171 answer_dict: dict[Optional[int], Optional[str]] = {None: None} 

172 for option in range(0, 4): 

173 answer_dict[option] = ( 

174 str(option) + " — " + self.wxstring(req, "a" + str(option)) 

175 ) 

176 

177 q_a = "" 

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

179 q_a += tr_qa( 

180 self.wxstring(req, "q" + str(q)), 

181 get_from_dict(answer_dict, getattr(self, "q" + str(q))), 

182 ) 

183 

184 return """ 

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

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

187 {tr_is_complete} 

188 {total_score} 

189 {anxiety_severity} 

190 </table> 

191 </div> 

192 <div class="{CssClass.EXPLANATION}"> 

193 Ratings are over the last 2 weeks. 

194 </div> 

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

196 <tr> 

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

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

199 </tr> 

200 {q_a} 

201 </table> 

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

203 [1] ≥15 severe, ≥10 moderate, ≥5 mild. 

204 Score ≥10 identifies: generalized anxiety disorder with 

205 sensitivity 89%, specificity 82% (Spitzer et al. 2006, PubMed 

206 ID 16717171); 

207 panic disorder with sensitivity 74%, specificity 81% (Kroenke 

208 et al. 2010, PMID 20633738); 

209 social anxiety with sensitivity 72%, specificity 80% (Kroenke 

210 et al. 2010); 

211 post-traumatic stress disorder with sensitivity 66%, 

212 specificity 81% (Kroenke et al. 2010). 

213 The majority of evidence contributing to these figures comes 

214 from primary care screening studies. 

215 </div> 

216 """.format( 

217 CssClass=CssClass, 

218 tr_is_complete=self.get_is_complete_tr(req), 

219 total_score=tr( 

220 req.sstring(SS.TOTAL_SCORE), 

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

222 ), 

223 anxiety_severity=tr( 

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

225 severity, 

226 ), 

227 q_a=q_a, 

228 ) 

229 

230 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: 

231 codes = [ 

232 SnomedExpression( 

233 req.snomed(SnomedLookup.GAD7_PROCEDURE_ASSESSMENT) 

234 ) 

235 ] 

236 if self.is_complete(): 

237 codes.append( 

238 SnomedExpression( 

239 req.snomed(SnomedLookup.GAD7_SCALE), 

240 {req.snomed(SnomedLookup.GAD7_SCORE): self.total_score()}, 

241 ) 

242 ) 

243 return codes