Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/phq15.py 

5 

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

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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

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

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

15 (at your option) any later version. 

16 

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

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

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

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

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

24 

25=============================================================================== 

26 

27""" 

28 

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

30 

31from cardinal_pythonlib.stringfunc import strseq 

32from sqlalchemy.ext.declarative import DeclarativeMeta 

33from sqlalchemy.sql.sqltypes import Integer 

34 

35from camcops_server.cc_modules.cc_constants import CssClass 

36from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

37from camcops_server.cc_modules.cc_db import add_multiple_columns 

38from camcops_server.cc_modules.cc_html import answer, get_yes_no, tr, tr_qa 

39from camcops_server.cc_modules.cc_request import CamcopsRequest 

40from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

41from camcops_server.cc_modules.cc_sqla_coltypes import SummaryCategoryColType 

42from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

43from camcops_server.cc_modules.cc_task import ( 

44 get_from_dict, 

45 Task, 

46 TaskHasPatientMixin, 

47) 

48from camcops_server.cc_modules.cc_text import SS 

49from camcops_server.cc_modules.cc_trackerhelpers import ( 

50 TrackerInfo, 

51 TrackerLabel, 

52) 

53 

54 

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

56# PHQ-15 

57# ============================================================================= 

58 

59class Phq15Metaclass(DeclarativeMeta): 

60 # noinspection PyInitNewSignature 

61 def __init__(cls: Type['Phq15'], 

62 name: str, 

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

64 classdict: Dict[str, Any]) -> None: 

65 add_multiple_columns( 

66 cls, "q", 1, cls.NQUESTIONS, 

67 minimum=0, maximum=2, 

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

69 "2 bothered a lot)", 

70 comment_strings=[ 

71 "stomach pain", 

72 "back pain", 

73 "limb/joint pain", 

74 "F - menstrual", 

75 "headaches", 

76 "chest pain", 

77 "dizziness", 

78 "fainting", 

79 "palpitations", 

80 "breathless", 

81 "sex", 

82 "constipation/diarrhoea", 

83 "nausea/indigestion", 

84 "energy", 

85 "sleep", 

86 ] 

87 ) 

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

89 

90 

91class Phq15(TaskHasPatientMixin, Task, 

92 metaclass=Phq15Metaclass): 

93 """ 

94 Server implementation of the PHQ-15 task. 

95 """ 

96 __tablename__ = "phq15" 

97 shortname = "PHQ-15" 

98 provides_trackers = True 

99 

100 NQUESTIONS = 15 

101 MAX_TOTAL = 30 

102 

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

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

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

106 

107 @staticmethod 

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

109 _ = req.gettext 

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

111 

112 # noinspection PyUnresolvedReferences 

113 def is_complete(self) -> bool: 

114 if not self.field_contents_valid(): 

115 return False 

116 if self.any_fields_none(self.ONE_TO_THREE): 

117 return False 

118 if self.any_fields_none(self.FIVE_TO_END): 

119 return False 

120 if self.is_female(): 

121 return self.q4 is not None 

122 else: 

123 return True 

124 

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

126 return [TrackerInfo( 

127 value=self.total_score(), 

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

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

130 axis_min=-0.5, 

131 axis_max=self.MAX_TOTAL + 0.5, 

132 horizontal_lines=[14.5, 9.5, 4.5], 

133 horizontal_labels=[ 

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

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

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

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

138 ] 

139 )] 

140 

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

142 if not self.is_complete(): 

143 return CTV_INCOMPLETE 

144 return [CtvInfo(content=( 

145 f"PHQ-15 total score {self.total_score()}/{self.MAX_TOTAL} " 

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

147 ))] 

148 

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

150 return self.standard_task_summary_fields() + [ 

151 SummaryElement(name="total", 

152 coltype=Integer(), 

153 value=self.total_score(), 

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

155 SummaryElement(name="severity", 

156 coltype=SummaryCategoryColType, 

157 value=self.severity(req), 

158 comment="Severity"), 

159 ] 

160 

161 def total_score(self) -> int: 

162 return self.sum_fields(self.TASK_FIELDS) 

163 

164 def num_severe(self) -> int: 

165 n = 0 

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

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

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

169 n += 1 

170 return n 

171 

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

173 score = self.total_score() 

174 if score >= 15: 

175 return req.sstring(SS.SEVERE) 

176 elif score >= 10: 

177 return req.sstring(SS.MODERATE) 

178 elif score >= 5: 

179 return req.sstring(SS.MILD) 

180 else: 

181 return req.sstring(SS.NONE) 

182 

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

184 score = self.total_score() 

185 nsevere = self.num_severe() 

186 somatoform_likely = nsevere >= 3 

187 severity = self.severity(req) 

188 answer_dict = {None: None} 

189 for option in range(0, 3): 

190 answer_dict[option] = str(option) + " – " + \ 

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

192 q_a = "" 

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

194 q_a += tr_qa( 

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

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

197 ) 

198 h = """ 

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

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

201 {tr_is_complete} 

202 {total_score} 

203 {n_severe_symptoms} 

204 {exceeds_somatoform_cutoff} 

205 {symptom_severity} 

206 </table> 

207 </div> 

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

209 <tr> 

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

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

212 </tr> 

213 {q_a} 

214 </table> 

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

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

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

218 [3] ≥3 severe symptoms. 

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

220 otherwise none. 

221 </div> 

222 """.format( 

223 CssClass=CssClass, 

224 tr_is_complete=self.get_is_complete_tr(req), 

225 total_score=tr( 

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

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

228 ), 

229 n_severe_symptoms=tr_qa( 

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

231 nsevere 

232 ), 

233 exceeds_somatoform_cutoff=tr_qa( 

234 self.wxstring(req, "exceeds_somatoform_cutoff") + 

235 " <sup>[3]</sup>", 

236 get_yes_no(req, somatoform_likely) 

237 ), 

238 symptom_severity=tr_qa( 

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

240 severity 

241 ), 

242 q_a=q_a, 

243 ) 

244 return h 

245 

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

247 procedure = req.snomed(SnomedLookup.PHQ15_PROCEDURE) 

248 codes = [SnomedExpression(procedure)] 

249 if self.is_complete(): 

250 scale = req.snomed(SnomedLookup.PHQ15_SCALE) 

251 score = req.snomed(SnomedLookup.PHQ15_SCORE) 

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

253 return codes