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

32from cardinal_pythonlib.stringfunc import strnumlist, 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_html import answer, tr 

39from camcops_server.cc_modules.cc_report import ( 

40 AverageScoreReport, 

41 ScoreDetails, 

42) 

43from camcops_server.cc_modules.cc_request import CamcopsRequest 

44from camcops_server.cc_modules.cc_sqla_coltypes import ( 

45 CamcopsColumn, 

46 PermittedValueChecker, 

47) 

48from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

49from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

50from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

51 

52 

53# ============================================================================= 

54# PBQ 

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

56 

57class PbqMetaclass(DeclarativeMeta): 

58 # noinspection PyInitNewSignature 

59 def __init__(cls: Type['Pbq'], 

60 name: str, 

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

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

63 comment_strings = [ 

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

65 # 1-5 

66 "I feel close to my baby", 

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

68 "I feel distant from my baby", 

69 "I love to cuddle my baby", 

70 "I regret having this baby", 

71 # 6-10 

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

73 "My baby winds me up", 

74 "I love my baby to bits", 

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

76 "My baby irritates me", 

77 # 11-15 

78 "I enjoy playing with my baby", 

79 "My baby cries too much", 

80 "I feel trapped as a mother", 

81 "I feel angry with my baby", 

82 "I resent my baby", 

83 # 16-20 

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

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

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

87 "My baby makes me anxious", 

88 "I am afraid of my baby", 

89 # 21-25 

90 "My baby annoys me", 

91 "I feel confident when changing my baby", 

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

93 "I feel like hurting my baby", 

94 "My baby is easily comforted", 

95 ] 

96 pvc = PermittedValueChecker(minimum=cls.MIN_PER_Q, 

97 maximum=cls.MAX_PER_Q) 

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

99 i = n - 1 

100 colname = f"q{n}" 

101 if n in cls.SCORED_A0N5_Q: 

102 explan = "always 0 - never 5" 

103 else: 

104 explan = "always 5 - never 0" 

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

106 setattr( 

107 cls, 

108 colname, 

109 CamcopsColumn(colname, Integer, 

110 comment=comment, permitted_value_checker=pvc) 

111 ) 

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

113 

114 

115class Pbq(TaskHasPatientMixin, Task, 

116 metaclass=PbqMetaclass): 

117 """ 

118 Server implementation of the PBQ task. 

119 """ 

120 __tablename__ = "pbq" 

121 shortname = "PBQ" 

122 provides_trackers = True 

123 

124 MIN_PER_Q = 0 

125 MAX_PER_Q = 5 

126 NQUESTIONS = 25 

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

128 MAX_TOTAL = MAX_PER_Q * NQUESTIONS 

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

130 FACTOR_1_Q = [1, 2, 6, 7, 8, 9, 10, 12, 13, 15, 16, 17] # 12 questions # noqa 

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

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

133 FACTOR_4_Q = [18, 24] # 2 questions 

134 FACTOR_1_F = strnumlist("q", FACTOR_1_Q) 

135 FACTOR_2_F = strnumlist("q", FACTOR_2_Q) 

136 FACTOR_3_F = strnumlist("q", FACTOR_3_Q) 

137 FACTOR_4_F = strnumlist("q", FACTOR_4_Q) 

138 FACTOR_1_MAX = len(FACTOR_1_Q) * MAX_PER_Q 

139 FACTOR_2_MAX = len(FACTOR_2_Q) * MAX_PER_Q 

140 FACTOR_3_MAX = len(FACTOR_3_Q) * MAX_PER_Q 

141 FACTOR_4_MAX = len(FACTOR_4_Q) * MAX_PER_Q 

142 

143 @staticmethod 

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

145 _ = req.gettext 

146 return _("Postpartum Bonding Questionnaire") 

147 

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

149 return [TrackerInfo( 

150 value=self.total_score(), 

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

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

153 axis_min=-0.5, 

154 axis_max=self.MAX_TOTAL + 0.5 

155 )] 

156 

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

158 return self.standard_task_summary_fields() + [ 

159 SummaryElement( 

160 name="total_score", coltype=Integer(), 

161 value=self.total_score(), 

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

163 ), 

164 SummaryElement( 

165 name="factor_1_score", coltype=Integer(), 

166 value=self.factor_1_score(), 

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

168 ), 

169 SummaryElement( 

170 name="factor_2_score", coltype=Integer(), 

171 value=self.factor_2_score(), 

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

173 ), 

174 SummaryElement( 

175 name="factor_3_score", coltype=Integer(), 

176 value=self.factor_3_score(), 

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

178 ), 

179 SummaryElement( 

180 name="factor_4_score", coltype=Integer(), 

181 value=self.factor_4_score(), 

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

183 ), 

184 ] 

185 

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

187 if not self.is_complete(): 

188 return CTV_INCOMPLETE 

189 return [CtvInfo(content=( 

190 f"PBQ total score {self.total_score()}/{self.MAX_TOTAL}. " 

191 f"Factor 1 score {self.factor_1_score()}/{self.FACTOR_1_MAX}. " 

192 f"Factor 2 score {self.factor_2_score()}/{self.FACTOR_2_MAX}. " 

193 f"Factor 3 score {self.factor_3_score()}/{self.FACTOR_3_MAX}. " 

194 f"Factor 4 score {self.factor_4_score()}/{self.FACTOR_4_MAX}." 

195 ))] 

196 

197 def total_score(self) -> int: 

198 return self.sum_fields(self.QUESTION_FIELDS) 

199 

200 def factor_1_score(self) -> int: 

201 return self.sum_fields(self.FACTOR_1_F) 

202 

203 def factor_2_score(self) -> int: 

204 return self.sum_fields(self.FACTOR_2_F) 

205 

206 def factor_3_score(self) -> int: 

207 return self.sum_fields(self.FACTOR_3_F) 

208 

209 def factor_4_score(self) -> int: 

210 return self.sum_fields(self.FACTOR_4_F) 

211 

212 def is_complete(self) -> bool: 

213 return ( 

214 self.field_contents_valid() and 

215 self.all_fields_not_none(self.QUESTION_FIELDS) 

216 ) 

217 

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

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

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

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

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

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

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

225 a0n5 = { 

226 0: always, 

227 1: very_often, 

228 2: quite_often, 

229 3: sometimes, 

230 4: rarely, 

231 5: never, 

232 } 

233 a5n0 = { 

234 5: always, 

235 4: very_often, 

236 3: quite_often, 

237 2: sometimes, 

238 1: rarely, 

239 0: never, 

240 } 

241 h = f""" 

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

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

244 {self.get_is_complete_tr(req)} 

245 <tr> 

246 <td>Total score</td> 

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

248 </td> 

249 <tr> 

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

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

252 </td> 

253 <tr> 

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

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

256 </td> 

257 <tr> 

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

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

260 </td> 

261 <tr> 

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

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

264 </td> 

265 </table> 

266 </div> 

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

268 <tr> 

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

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

271 </tr> 

272 """ # noqa 

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

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

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

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

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

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

279 h += f""" 

280 </table> 

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

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

283 PMID 16673041), as follows. 

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

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

286 [2] Factor examining severe mother–infant relationship 

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

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

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

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

291 high; based on questions 

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

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

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

295 known low sensitivity; based on questions 

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

297 </div> 

298 """ 

299 return h 

300 

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

302 

303 

304class PBQReport(AverageScoreReport): 

305 # noinspection PyMethodParameters 

306 @classproperty 

307 def report_id(cls) -> str: 

308 return "PBQ" 

309 

310 @classmethod 

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

312 _ = req.gettext 

313 return _("PBQ — Average scores") 

314 

315 # noinspection PyMethodParameters 

316 @classproperty 

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

318 return Pbq 

319 

320 @classmethod 

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

322 _ = req.gettext 

323 return [ 

324 ScoreDetails( 

325 name=_("Total score"), 

326 scorefunc=Pbq.total_score, 

327 minimum=0, 

328 maximum=Pbq.MAX_TOTAL, 

329 higher_score_is_better=False 

330 ), 

331 ScoreDetails( 

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

333 scorefunc=Pbq.factor_1_score, 

334 minimum=0, 

335 maximum=Pbq.FACTOR_1_MAX, 

336 higher_score_is_better=False 

337 ), 

338 ScoreDetails( 

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

340 scorefunc=Pbq.factor_2_score, 

341 minimum=0, 

342 maximum=Pbq.FACTOR_2_MAX, 

343 higher_score_is_better=False 

344 ), 

345 ScoreDetails( 

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

347 scorefunc=Pbq.factor_3_score, 

348 minimum=0, 

349 maximum=Pbq.FACTOR_3_MAX, 

350 higher_score_is_better=False 

351 ), 

352 ScoreDetails( 

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

354 scorefunc=Pbq.factor_4_score, 

355 minimum=0, 

356 maximum=Pbq.FACTOR_4_MAX, 

357 higher_score_is_better=False 

358 ), 

359 ]