Coverage for tasks/phq15.py: 44%

90 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/phq15.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, get_yes_no, tr, tr_qa 

40from camcops_server.cc_modules.cc_request import CamcopsRequest 

41from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

42from camcops_server.cc_modules.cc_sqla_coltypes import SummaryCategoryColType 

43from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

44from camcops_server.cc_modules.cc_task import ( 

45 get_from_dict, 

46 Task, 

47 TaskHasPatientMixin, 

48) 

49from camcops_server.cc_modules.cc_text import SS 

50from camcops_server.cc_modules.cc_trackerhelpers import ( 

51 TrackerInfo, 

52 TrackerLabel, 

53) 

54 

55 

56# ============================================================================= 

57# PHQ-15 

58# ============================================================================= 

59 

60 

61class Phq15Metaclass(DeclarativeMeta): 

62 # noinspection PyInitNewSignature 

63 def __init__( 

64 cls: Type["Phq15"], 

65 name: str, 

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

67 classdict: Dict[str, Any], 

68 ) -> None: 

69 add_multiple_columns( 

70 cls, 

71 "q", 

72 1, 

73 cls.NQUESTIONS, 

74 minimum=0, 

75 maximum=2, 

76 comment_fmt="Q{n} ({s}) (0 not bothered at all - " 

77 "2 bothered a lot)", 

78 comment_strings=[ 

79 "stomach pain", 

80 "back pain", 

81 "limb/joint pain", 

82 "F - menstrual", 

83 "headaches", 

84 "chest pain", 

85 "dizziness", 

86 "fainting", 

87 "palpitations", 

88 "breathless", 

89 "sex", 

90 "constipation/diarrhoea", 

91 "nausea/indigestion", 

92 "energy", 

93 "sleep", 

94 ], 

95 ) 

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

97 

98 

99class Phq15(TaskHasPatientMixin, Task, metaclass=Phq15Metaclass): 

100 """ 

101 Server implementation of the PHQ-15 task. 

102 """ 

103 

104 __tablename__ = "phq15" 

105 shortname = "PHQ-15" 

106 provides_trackers = True 

107 

108 NQUESTIONS = 15 

109 MAX_TOTAL = 30 

110 

111 ONE_TO_THREE = strseq("q", 1, 3) 

112 FIVE_TO_END = strseq("q", 5, NQUESTIONS) 

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

114 

115 @staticmethod 

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

117 _ = req.gettext 

118 return _("Patient Health Questionnaire-15") 

119 

120 # noinspection PyUnresolvedReferences 

121 def is_complete(self) -> bool: 

122 if not self.field_contents_valid(): 

123 return False 

124 if self.any_fields_none(self.ONE_TO_THREE): 

125 return False 

126 if self.any_fields_none(self.FIVE_TO_END): 

127 return False 

128 if self.is_female(): 

129 return self.q4 is not None 

130 else: 

131 return True 

132 

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

134 return [ 

135 TrackerInfo( 

136 value=self.total_score(), 

137 plot_label="PHQ-15 total score (rating somatic symptoms)", 

138 axis_label=f"Score for Q1-15 (out of {self.MAX_TOTAL})", 

139 axis_min=-0.5, 

140 axis_max=self.MAX_TOTAL + 0.5, 

141 horizontal_lines=[14.5, 9.5, 4.5], 

142 horizontal_labels=[ 

143 TrackerLabel(22, req.sstring(SS.SEVERE)), 

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

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

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

147 ], 

148 ) 

149 ] 

150 

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

152 if not self.is_complete(): 

153 return CTV_INCOMPLETE 

154 return [ 

155 CtvInfo( 

156 content=( 

157 f"PHQ-15 total score {self.total_score()}/{self.MAX_TOTAL} " # noqa: E501 

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

159 ) 

160 ) 

161 ] 

162 

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

164 return self.standard_task_summary_fields() + [ 

165 SummaryElement( 

166 name="total", 

167 coltype=Integer(), 

168 value=self.total_score(), 

169 comment=f"Total score (/{self.MAX_TOTAL})", 

170 ), 

171 SummaryElement( 

172 name="severity", 

173 coltype=SummaryCategoryColType, 

174 value=self.severity(req), 

175 comment="Severity", 

176 ), 

177 ] 

178 

179 def total_score(self) -> int: 

180 return self.sum_fields(self.TASK_FIELDS) 

181 

182 def num_severe(self) -> int: 

183 n = 0 

184 for i in range(1, self.NQUESTIONS + 1): 

185 value = getattr(self, "q" + str(i)) 

186 if value is not None and value >= 2: 

187 n += 1 

188 return n 

189 

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

191 score = self.total_score() 

192 if score >= 15: 

193 return req.sstring(SS.SEVERE) 

194 elif score >= 10: 

195 return req.sstring(SS.MODERATE) 

196 elif score >= 5: 

197 return req.sstring(SS.MILD) 

198 else: 

199 return req.sstring(SS.NONE) 

200 

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

202 score = self.total_score() 

203 nsevere = self.num_severe() 

204 somatoform_likely = nsevere >= 3 

205 severity = self.severity(req) 

206 answer_dict = {None: None} 

207 for option in range(0, 3): 

208 answer_dict[option] = ( 

209 str(option) + " – " + self.wxstring(req, "a" + str(option)) 

210 ) 

211 q_a = "" 

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

213 q_a += tr_qa( 

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

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

216 ) 

217 h = """ 

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

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

220 {tr_is_complete} 

221 {total_score} 

222 {n_severe_symptoms} 

223 {exceeds_somatoform_cutoff} 

224 {symptom_severity} 

225 </table> 

226 </div> 

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

228 <tr> 

229 <th width="70%">Question</th> 

230 <th width="30%">Answer</th> 

231 </tr> 

232 {q_a} 

233 </table> 

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

235 [1] In males, maximum score is actually 28. 

236 [2] Questions with scores ≥2 are considered severe. 

237 [3] ≥3 severe symptoms. 

238 [4] Total score ≥15 severe, ≥10 moderate, ≥5 mild, 

239 otherwise none. 

240 </div> 

241 """.format( 

242 CssClass=CssClass, 

243 tr_is_complete=self.get_is_complete_tr(req), 

244 total_score=tr( 

245 req.sstring(SS.TOTAL_SCORE) + " <sup>[1]</sup>", 

246 answer(score) + f" / {self.MAX_TOTAL}", 

247 ), 

248 n_severe_symptoms=tr_qa( 

249 self.wxstring(req, "n_severe_symptoms") + " <sup>[2]</sup>", 

250 nsevere, 

251 ), 

252 exceeds_somatoform_cutoff=tr_qa( 

253 self.wxstring(req, "exceeds_somatoform_cutoff") 

254 + " <sup>[3]</sup>", 

255 get_yes_no(req, somatoform_likely), 

256 ), 

257 symptom_severity=tr_qa( 

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

259 severity, 

260 ), 

261 q_a=q_a, 

262 ) 

263 return h 

264 

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

266 procedure = req.snomed(SnomedLookup.PHQ15_PROCEDURE) 

267 codes = [SnomedExpression(procedure)] 

268 if self.is_complete(): 

269 scale = req.snomed(SnomedLookup.PHQ15_SCALE) 

270 score = req.snomed(SnomedLookup.PHQ15_SCORE) 

271 codes.append(SnomedExpression(scale, {score: self.total_score()})) 

272 return codes