Coverage for tasks/pbq.py: 66%

107 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/pbq.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.classes import classproperty 

33from cardinal_pythonlib.stringfunc import strnumlist, strseq 

34from sqlalchemy.ext.declarative import DeclarativeMeta 

35from sqlalchemy.sql.sqltypes import Integer 

36 

37from camcops_server.cc_modules.cc_constants import CssClass 

38from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

39from camcops_server.cc_modules.cc_html import answer, tr 

40from camcops_server.cc_modules.cc_report import ( 

41 AverageScoreReport, 

42 ScoreDetails, 

43) 

44from camcops_server.cc_modules.cc_request import CamcopsRequest 

45from camcops_server.cc_modules.cc_sqla_coltypes import ( 

46 CamcopsColumn, 

47 PermittedValueChecker, 

48) 

49from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

50from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

51from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

52 

53 

54# ============================================================================= 

55# PBQ 

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

57 

58 

59class PbqMetaclass(DeclarativeMeta): 

60 # noinspection PyInitNewSignature 

61 def __init__( 

62 cls: Type["Pbq"], 

63 name: str, 

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

65 classdict: Dict[str, Any], 

66 ) -> None: 

67 comment_strings = [ 

68 # This is the Brockington 2006 order; see XML for notes. 

69 # 1-5 

70 "I feel close to my baby", 

71 "I wish the old days when I had no baby would come back", 

72 "I feel distant from my baby", 

73 "I love to cuddle my baby", 

74 "I regret having this baby", 

75 # 6-10 

76 "The baby does not seem to be mine", 

77 "My baby winds me up", 

78 "I love my baby to bits", 

79 "I feel happy when my baby smiles or laughs", 

80 "My baby irritates me", 

81 # 11-15 

82 "I enjoy playing with my baby", 

83 "My baby cries too much", 

84 "I feel trapped as a mother", 

85 "I feel angry with my baby", 

86 "I resent my baby", 

87 # 16-20 

88 "My baby is the most beautiful baby in the world", 

89 "I wish my baby would somehow go away", 

90 "I have done harmful things to my baby", 

91 "My baby makes me anxious", 

92 "I am afraid of my baby", 

93 # 21-25 

94 "My baby annoys me", 

95 "I feel confident when changing my baby", 

96 "I feel the only solution is for someone else to look after my baby", # noqa 

97 "I feel like hurting my baby", 

98 "My baby is easily comforted", 

99 ] 

100 pvc = PermittedValueChecker( 

101 minimum=cls.MIN_PER_Q, maximum=cls.MAX_PER_Q 

102 ) 

103 for n in range(1, cls.NQUESTIONS + 1): 

104 i = n - 1 

105 colname = f"q{n}" 

106 if n in cls.SCORED_A0N5_Q: 

107 explan = "always 0 - never 5" 

108 else: 

109 explan = "always 5 - never 0" 

110 comment = f"Q{n}, {comment_strings[i]} ({explan}, higher worse)" 

111 setattr( 

112 cls, 

113 colname, 

114 CamcopsColumn( 

115 colname, 

116 Integer, 

117 comment=comment, 

118 permitted_value_checker=pvc, 

119 ), 

120 ) 

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

122 

123 

124class Pbq(TaskHasPatientMixin, Task, metaclass=PbqMetaclass): 

125 """ 

126 Server implementation of the PBQ task. 

127 """ 

128 

129 __tablename__ = "pbq" 

130 shortname = "PBQ" 

131 provides_trackers = True 

132 

133 MIN_PER_Q = 0 

134 MAX_PER_Q = 5 

135 NQUESTIONS = 25 

136 QUESTION_FIELDS = strseq("q", 1, NQUESTIONS) 

137 MAX_TOTAL = MAX_PER_Q * NQUESTIONS 

138 SCORED_A0N5_Q = [1, 4, 8, 9, 11, 16, 22, 25] # rest scored A5N0 

139 FACTOR_1_Q = [ 

140 1, 

141 2, 

142 6, 

143 7, 

144 8, 

145 9, 

146 10, 

147 12, 

148 13, 

149 15, 

150 16, 

151 17, 

152 ] # 12 questions # noqa 

153 FACTOR_2_Q = [3, 4, 5, 11, 14, 21, 23] # 7 questions 

154 FACTOR_3_Q = [19, 20, 22, 25] # 4 questions 

155 FACTOR_4_Q = [18, 24] # 2 questions 

156 FACTOR_1_F = strnumlist("q", FACTOR_1_Q) 

157 FACTOR_2_F = strnumlist("q", FACTOR_2_Q) 

158 FACTOR_3_F = strnumlist("q", FACTOR_3_Q) 

159 FACTOR_4_F = strnumlist("q", FACTOR_4_Q) 

160 FACTOR_1_MAX = len(FACTOR_1_Q) * MAX_PER_Q 

161 FACTOR_2_MAX = len(FACTOR_2_Q) * MAX_PER_Q 

162 FACTOR_3_MAX = len(FACTOR_3_Q) * MAX_PER_Q 

163 FACTOR_4_MAX = len(FACTOR_4_Q) * MAX_PER_Q 

164 

165 @staticmethod 

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

167 _ = req.gettext 

168 return _("Postpartum Bonding Questionnaire") 

169 

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

171 return [ 

172 TrackerInfo( 

173 value=self.total_score(), 

174 plot_label="PBQ total score (lower is better)", 

175 axis_label=f"Total score (out of {self.MAX_TOTAL})", 

176 axis_min=-0.5, 

177 axis_max=self.MAX_TOTAL + 0.5, 

178 ) 

179 ] 

180 

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

182 return self.standard_task_summary_fields() + [ 

183 SummaryElement( 

184 name="total_score", 

185 coltype=Integer(), 

186 value=self.total_score(), 

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

188 ), 

189 SummaryElement( 

190 name="factor_1_score", 

191 coltype=Integer(), 

192 value=self.factor_1_score(), 

193 comment=f"Factor 1 score (/ {self.FACTOR_1_MAX})", 

194 ), 

195 SummaryElement( 

196 name="factor_2_score", 

197 coltype=Integer(), 

198 value=self.factor_2_score(), 

199 comment=f"Factor 2 score (/ {self.FACTOR_2_MAX})", 

200 ), 

201 SummaryElement( 

202 name="factor_3_score", 

203 coltype=Integer(), 

204 value=self.factor_3_score(), 

205 comment=f"Factor 3 score (/ {self.FACTOR_3_MAX})", 

206 ), 

207 SummaryElement( 

208 name="factor_4_score", 

209 coltype=Integer(), 

210 value=self.factor_4_score(), 

211 comment=f"Factor 4 score (/ {self.FACTOR_4_MAX})", 

212 ), 

213 ] 

214 

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

216 if not self.is_complete(): 

217 return CTV_INCOMPLETE 

218 return [ 

219 CtvInfo( 

220 content=( 

221 f"PBQ total score " 

222 f"{self.total_score()}/{self.MAX_TOTAL}. " 

223 f"Factor 1 score " 

224 f"{self.factor_1_score()}/{self.FACTOR_1_MAX}. " 

225 f"Factor 2 score " 

226 f"{self.factor_2_score()}/{self.FACTOR_2_MAX}. " 

227 f"Factor 3 score " 

228 f"{self.factor_3_score()}/{self.FACTOR_3_MAX}. " 

229 f"Factor 4 score " 

230 f"{self.factor_4_score()}/{self.FACTOR_4_MAX}." 

231 ) 

232 ) 

233 ] 

234 

235 def total_score(self) -> int: 

236 return self.sum_fields(self.QUESTION_FIELDS) 

237 

238 def factor_1_score(self) -> int: 

239 return self.sum_fields(self.FACTOR_1_F) 

240 

241 def factor_2_score(self) -> int: 

242 return self.sum_fields(self.FACTOR_2_F) 

243 

244 def factor_3_score(self) -> int: 

245 return self.sum_fields(self.FACTOR_3_F) 

246 

247 def factor_4_score(self) -> int: 

248 return self.sum_fields(self.FACTOR_4_F) 

249 

250 def is_complete(self) -> bool: 

251 return self.field_contents_valid() and self.all_fields_not_none( 

252 self.QUESTION_FIELDS 

253 ) 

254 

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

256 always = self.xstring(req, "always") 

257 very_often = self.xstring(req, "very_often") 

258 quite_often = self.xstring(req, "quite_often") 

259 sometimes = self.xstring(req, "sometimes") 

260 rarely = self.xstring(req, "rarely") 

261 never = self.xstring(req, "never") 

262 a0n5 = { 

263 0: always, 

264 1: very_often, 

265 2: quite_often, 

266 3: sometimes, 

267 4: rarely, 

268 5: never, 

269 } 

270 a5n0 = { 

271 5: always, 

272 4: very_often, 

273 3: quite_often, 

274 2: sometimes, 

275 1: rarely, 

276 0: never, 

277 } 

278 h = f""" 

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

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

281 {self.get_is_complete_tr(req)} 

282 <tr> 

283 <td>Total score</td> 

284 <td>{answer(self.total_score())} / {self.MAX_TOTAL}</td> 

285 </td> 

286 <tr> 

287 <td>Factor 1 score <sup>[1]</sup></td> 

288 <td>{answer(self.factor_1_score())} / {self.FACTOR_1_MAX}</td> 

289 </td> 

290 <tr> 

291 <td>Factor 2 score <sup>[2]</sup></td> 

292 <td>{answer(self.factor_2_score())} / {self.FACTOR_2_MAX}</td> 

293 </td> 

294 <tr> 

295 <td>Factor 3 score <sup>[3]</sup></td> 

296 <td>{answer(self.factor_3_score())} / {self.FACTOR_3_MAX}</td> 

297 </td> 

298 <tr> 

299 <td>Factor 4 score <sup>[4]</sup></td> 

300 <td>{answer(self.factor_4_score())} / {self.FACTOR_4_MAX}</td> 

301 </td> 

302 </table> 

303 </div> 

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

305 <tr> 

306 <th width="60%">Question</th> 

307 <th width="40%">Answer ({self.MIN_PER_Q}–{self.MAX_PER_Q})</th> 

308 </tr> 

309 """ # noqa 

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

311 qtext = f"{q}. " + self.wxstring(req, f"q{q}") 

312 a = getattr(self, f"q{q}") 

313 option = a0n5.get(a) if q in self.SCORED_A0N5_Q else a5n0.get(a) 

314 atext = f"{a}: {option}" 

315 h += tr(qtext, answer(atext)) 

316 h += f""" 

317 </table> 

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

319 Factors and cut-off scores are from Brockington et al. (2006, 

320 PMID 16673041), as follows. 

321 [1] General factor; ≤11 normal, ≥12 high; based on questions 

322 {", ".join(str(x) for x in self.FACTOR_1_Q)}. 

323 [2] Factor examining severe mother–infant relationship 

324 disorders; ≤12 normal, ≥13 high (cf. original 2001 study 

325 with ≤16 normal, ≥17 high); based on questions 

326 {", ".join(str(x) for x in self.FACTOR_2_Q)}. 

327 [3] Factor relating to infant-focused anxiety; ≤9 normal, ≥10 

328 high; based on questions 

329 {", ".join(str(x) for x in self.FACTOR_3_Q)}. 

330 [4] Factor relating to thoughts of harm to infant; ≤1 normal, 

331 ≥2 high (cf. original 2001 study with ≤2 normal, ≥3 high); 

332 known low sensitivity; based on questions 

333 {", ".join(str(x) for x in self.FACTOR_4_Q)}. 

334 </div> 

335 """ 

336 return h 

337 

338 # No SNOMED codes for the PBQ (checked 2019-04-01). 

339 

340 

341class PBQReport(AverageScoreReport): 

342 # noinspection PyMethodParameters 

343 @classproperty 

344 def report_id(cls) -> str: 

345 return "PBQ" 

346 

347 @classmethod 

348 def title(cls, req: "CamcopsRequest") -> str: 

349 _ = req.gettext 

350 return _("PBQ — Average scores") 

351 

352 # noinspection PyMethodParameters 

353 @classproperty 

354 def task_class(cls) -> Type[Task]: 

355 return Pbq 

356 

357 @classmethod 

358 def scoretypes(cls, req: "CamcopsRequest") -> List[ScoreDetails]: 

359 _ = req.gettext 

360 return [ 

361 ScoreDetails( 

362 name=_("Total score"), 

363 scorefunc=Pbq.total_score, 

364 minimum=0, 

365 maximum=Pbq.MAX_TOTAL, 

366 higher_score_is_better=False, 

367 ), 

368 ScoreDetails( 

369 name=_("Factor 1 score"), 

370 scorefunc=Pbq.factor_1_score, 

371 minimum=0, 

372 maximum=Pbq.FACTOR_1_MAX, 

373 higher_score_is_better=False, 

374 ), 

375 ScoreDetails( 

376 name=_("Factor 2 score"), 

377 scorefunc=Pbq.factor_2_score, 

378 minimum=0, 

379 maximum=Pbq.FACTOR_2_MAX, 

380 higher_score_is_better=False, 

381 ), 

382 ScoreDetails( 

383 name=_("Factor 3 score"), 

384 scorefunc=Pbq.factor_3_score, 

385 minimum=0, 

386 maximum=Pbq.FACTOR_3_MAX, 

387 higher_score_is_better=False, 

388 ), 

389 ScoreDetails( 

390 name=_("Factor 4 score"), 

391 scorefunc=Pbq.factor_4_score, 

392 minimum=0, 

393 maximum=Pbq.FACTOR_4_MAX, 

394 higher_score_is_better=False, 

395 ), 

396 ]