Coverage for tasks/pcl.py: 58%

111 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/pcl.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 abc import ABCMeta, ABC 

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

32 

33from cardinal_pythonlib.stringfunc import strseq 

34from sqlalchemy.ext.declarative import DeclarativeMeta 

35from sqlalchemy.sql.schema import Column 

36from sqlalchemy.sql.sqltypes import Boolean, Integer, UnicodeText 

37 

38from camcops_server.cc_modules.cc_constants import CssClass 

39from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

40from camcops_server.cc_modules.cc_db import add_multiple_columns 

41from camcops_server.cc_modules.cc_html import ( 

42 answer, 

43 get_yes_no, 

44 subheading_spanning_two_columns, 

45 tr, 

46 tr_qa, 

47) 

48from camcops_server.cc_modules.cc_request import CamcopsRequest 

49from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

50from camcops_server.cc_modules.cc_task import ( 

51 get_from_dict, 

52 Task, 

53 TaskHasPatientMixin, 

54) 

55from camcops_server.cc_modules.cc_text import SS 

56from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

57 

58 

59# ============================================================================= 

60# PCL 

61# ============================================================================= 

62 

63 

64class PclMetaclass(DeclarativeMeta, ABCMeta): 

65 """ 

66 There is a multilayer metaclass problem; see hads.py for discussion. 

67 """ 

68 

69 # noinspection PyInitNewSignature 

70 def __init__( 

71 cls: Type["PclCommon"], 

72 name: str, 

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

74 classdict: Dict[str, Any], 

75 ) -> None: 

76 add_multiple_columns( 

77 cls, 

78 "q", 

79 1, 

80 cls.NQUESTIONS, 

81 minimum=1, 

82 maximum=5, 

83 comment_fmt="Q{n} ({s}) (1 not at all - 5 extremely)", 

84 comment_strings=[ 

85 "disturbing memories/thoughts/images", 

86 "disturbing dreams", 

87 "reliving", 

88 "upset at reminders", 

89 "physical reactions to reminders", 

90 "avoid thinking/talking/feelings relating to experience", 

91 "avoid activities/situations because they remind", 

92 "trouble remembering important parts of stressful event", 

93 "loss of interest in previously enjoyed activities", 

94 "feeling distant/cut off from people", 

95 "feeling emotionally numb", 

96 "feeling future will be cut short", 

97 "hard to sleep", 

98 "irritable", 

99 "difficulty concentrating", 

100 "super alert/on guard", 

101 "jumpy/easily startled", 

102 ], 

103 ) 

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

105 

106 

107class PclCommon(TaskHasPatientMixin, Task, ABC, metaclass=PclMetaclass): 

108 __abstract__ = True 

109 provides_trackers = True 

110 extrastring_taskname = "pcl" 

111 info_filename_stem = extrastring_taskname 

112 

113 NQUESTIONS = 17 

114 SCORED_FIELDS = strseq("q", 1, NQUESTIONS) 

115 TASK_FIELDS = SCORED_FIELDS # may be overridden 

116 TASK_TYPE = "?" # will be overridden 

117 # ... not really used; we display the generic question forms on the server 

118 MIN_SCORE = NQUESTIONS 

119 MAX_SCORE = 5 * NQUESTIONS 

120 

121 def is_complete(self) -> bool: 

122 return ( 

123 self.all_fields_not_none(self.TASK_FIELDS) 

124 and self.field_contents_valid() 

125 ) 

126 

127 def total_score(self) -> int: 

128 return self.sum_fields(self.SCORED_FIELDS) 

129 

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

131 return [ 

132 TrackerInfo( 

133 value=self.total_score(), 

134 plot_label="PCL total score", 

135 axis_label=f"Total score ({self.MIN_SCORE}-{self.MAX_SCORE})", 

136 axis_min=self.MIN_SCORE - 0.5, 

137 axis_max=self.MAX_SCORE + 0.5, 

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=f"PCL total score {self.total_score()}")] 

145 

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

147 return self.standard_task_summary_fields() + [ 

148 SummaryElement( 

149 name="total", 

150 coltype=Integer(), 

151 value=self.total_score(), 

152 comment=f"Total score ({self.MIN_SCORE}-{self.MAX_SCORE})", 

153 ), 

154 SummaryElement( 

155 name="num_symptomatic", 

156 coltype=Integer(), 

157 value=self.num_symptomatic(), 

158 comment="Total number of symptoms considered symptomatic " 

159 "(meaning scoring 3 or more)", 

160 ), 

161 SummaryElement( 

162 name="num_symptomatic_B", 

163 coltype=Integer(), 

164 value=self.num_symptomatic_b(), 

165 comment="Number of group B symptoms considered symptomatic " 

166 "(meaning scoring 3 or more)", 

167 ), 

168 SummaryElement( 

169 name="num_symptomatic_C", 

170 coltype=Integer(), 

171 value=self.num_symptomatic_c(), 

172 comment="Number of group C symptoms considered symptomatic " 

173 "(meaning scoring 3 or more)", 

174 ), 

175 SummaryElement( 

176 name="num_symptomatic_D", 

177 coltype=Integer(), 

178 value=self.num_symptomatic_d(), 

179 comment="Number of group D symptoms considered symptomatic " 

180 "(meaning scoring 3 or more)", 

181 ), 

182 SummaryElement( 

183 name="ptsd", 

184 coltype=Boolean(), 

185 value=self.ptsd(), 

186 comment="Meets DSM-IV criteria for PTSD", 

187 ), 

188 ] 

189 

190 def get_num_symptomatic(self, first: int, last: int) -> int: 

191 n = 0 

192 for i in range(first, last + 1): 

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

194 if value is not None and value >= 3: 

195 n += 1 

196 return n 

197 

198 def num_symptomatic(self) -> int: 

199 return self.get_num_symptomatic(1, self.NQUESTIONS) 

200 

201 def num_symptomatic_b(self) -> int: 

202 return self.get_num_symptomatic(1, 5) 

203 

204 def num_symptomatic_c(self) -> int: 

205 return self.get_num_symptomatic(6, 12) 

206 

207 def num_symptomatic_d(self) -> int: 

208 return self.get_num_symptomatic(13, 17) 

209 

210 def ptsd(self) -> bool: 

211 num_symptomatic_b = self.num_symptomatic_b() 

212 num_symptomatic_c = self.num_symptomatic_c() 

213 num_symptomatic_d = self.num_symptomatic_d() 

214 return ( 

215 num_symptomatic_b >= 1 

216 and num_symptomatic_c >= 3 

217 and num_symptomatic_d >= 2 

218 ) 

219 

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

221 score = self.total_score() 

222 num_symptomatic = self.num_symptomatic() 

223 num_symptomatic_b = self.num_symptomatic_b() 

224 num_symptomatic_c = self.num_symptomatic_c() 

225 num_symptomatic_d = self.num_symptomatic_d() 

226 ptsd = self.ptsd() 

227 answer_dict = {None: None} 

228 for option in range(1, 6): 

229 answer_dict[option] = ( 

230 str(option) 

231 + " – " 

232 + self.wxstring(req, "option" + str(option)) 

233 ) 

234 q_a = "" 

235 if hasattr(self, "event") and hasattr(self, "eventdate"): 

236 # PCL-S 

237 q_a += tr_qa(self.wxstring(req, "s_event_s"), self.event) 

238 q_a += tr_qa(self.wxstring(req, "s_eventdate_s"), self.eventdate) 

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

240 if q == 1 or q == 6 or q == 13: 

241 section = "B" if q == 1 else ("C" if q == 6 else "D") 

242 q_a += subheading_spanning_two_columns( 

243 f"DSM-IV-TR section {section}" 

244 ) 

245 q_a += tr_qa( 

246 self.wxstring(req, "q" + str(q) + "_s"), 

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

248 ) 

249 h = """ 

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

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

252 {tr_is_complete} 

253 {total_score} 

254 {num_symptomatic} 

255 {dsm_criteria_met} 

256 </table> 

257 </div> 

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

259 <tr> 

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

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

262 </tr> 

263 {q_a} 

264 </table> 

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

266 [1] Questions with scores ≥3 are considered symptomatic. 

267 [2] ≥1 ‘B’ symptoms and ≥3 ‘C’ symptoms and 

268 ≥2 ‘D’ symptoms. 

269 </div> 

270 """.format( 

271 CssClass=CssClass, 

272 tr_is_complete=self.get_is_complete_tr(req), 

273 total_score=tr_qa(f"{req.sstring(SS.TOTAL_SCORE)} (17–85)", score), 

274 num_symptomatic=tr( 

275 "Number symptomatic <sup>[1]</sup>: B, C, D (total)", 

276 answer(num_symptomatic_b) 

277 + ", " 

278 + answer(num_symptomatic_c) 

279 + ", " 

280 + answer(num_symptomatic_d) 

281 + " (" 

282 + answer(num_symptomatic) 

283 + ")", 

284 ), 

285 dsm_criteria_met=tr_qa( 

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

287 get_yes_no(req, ptsd), 

288 ), 

289 q_a=q_a, 

290 ) 

291 return h 

292 

293 

294# ============================================================================= 

295# PCL-C 

296# ============================================================================= 

297 

298 

299class PclC(PclCommon, metaclass=PclMetaclass): 

300 """ 

301 Server implementation of the PCL-C task. 

302 """ 

303 

304 __tablename__ = "pclc" 

305 shortname = "PCL-C" 

306 

307 TASK_TYPE = "C" 

308 

309 @staticmethod 

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

311 _ = req.gettext 

312 return _("PTSD Checklist, Civilian version") 

313 

314 

315# ============================================================================= 

316# PCL-M 

317# ============================================================================= 

318 

319 

320class PclM(PclCommon, metaclass=PclMetaclass): 

321 """ 

322 Server implementation of the PCL-M task. 

323 """ 

324 

325 __tablename__ = "pclm" 

326 shortname = "PCL-M" 

327 

328 TASK_TYPE = "M" 

329 

330 @staticmethod 

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

332 _ = req.gettext 

333 return _("PTSD Checklist, Military version") 

334 

335 

336# ============================================================================= 

337# PCL-S 

338# ============================================================================= 

339 

340 

341class PclS(PclCommon, metaclass=PclMetaclass): 

342 """ 

343 Server implementation of the PCL-S task. 

344 """ 

345 

346 __tablename__ = "pcls" 

347 shortname = "PCL-S" 

348 

349 event = Column("event", UnicodeText, comment="Traumatic event") 

350 eventdate = Column( 

351 "eventdate", UnicodeText, comment="Date of traumatic event (free text)" 

352 ) 

353 

354 TASK_FIELDS = PclCommon.SCORED_FIELDS + ["event", "eventdate"] 

355 TASK_TYPE = "S" 

356 

357 @staticmethod 

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

359 _ = req.gettext 

360 return _("PTSD Checklist, Stressor-specific version")