Coverage for tasks/gad7.py: 52%

1"""

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

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 CTV_INCOMPLETE, CtvInfo

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_snomed import SnomedExpression, SnomedLookup

39 from camcops_server.cc_modules.cc_sqla_coltypes import SummaryCategoryColType

40 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

41 from camcops_server.cc_modules.cc_task import (

42 get_from_dict,

43 Task,

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

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

56

57

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