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

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

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.schema import Column 

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

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_db import add_multiple_columns 

40from camcops_server.cc_modules.cc_html import ( 

41 answer, 

42 get_yes_no, 

43 subheading_spanning_two_columns, 

44 tr, 

45 tr_qa, 

46) 

47from camcops_server.cc_modules.cc_request import CamcopsRequest 

48from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

49from camcops_server.cc_modules.cc_task import ( 

50 get_from_dict, 

51 Task, 

52 TaskHasPatientMixin, 

53) 

54from camcops_server.cc_modules.cc_text import SS 

55from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

56 

57 

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

59# PCL 

60# ============================================================================= 

61 

62class PclMetaclass(DeclarativeMeta, ABCMeta): 

63 """ 

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

65 """ 

66 # noinspection PyInitNewSignature 

67 def __init__(cls: Type['PclCommon'], 

68 name: str, 

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

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

71 add_multiple_columns( 

72 cls, "q", 1, cls.NQUESTIONS, 

73 minimum=1, maximum=5, 

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

75 comment_strings=[ 

76 "disturbing memories/thoughts/images", 

77 "disturbing dreams", 

78 "reliving", 

79 "upset at reminders", 

80 "physical reactions to reminders", 

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

82 "avoid activities/situations because they remind", 

83 "trouble remembering important parts of stressful event", 

84 "loss of interest in previously enjoyed activities", 

85 "feeling distant/cut off from people", 

86 "feeling emotionally numb", 

87 "feeling future will be cut short", 

88 "hard to sleep", 

89 "irritable", 

90 "difficulty concentrating", 

91 "super alert/on guard", 

92 "jumpy/easily startled", 

93 ] 

94 ) 

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

96 

97 

98class PclCommon(TaskHasPatientMixin, Task, ABC, 

99 metaclass=PclMetaclass): 

100 __abstract__ = True 

101 provides_trackers = True 

102 extrastring_taskname = "pcl" 

103 

104 NQUESTIONS = 17 

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

106 TASK_FIELDS = SCORED_FIELDS # may be overridden 

107 TASK_TYPE = "?" # will be overridden 

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

109 MIN_SCORE = NQUESTIONS 

110 MAX_SCORE = 5 * NQUESTIONS 

111 

112 def is_complete(self) -> bool: 

113 return ( 

114 self.all_fields_not_none(self.TASK_FIELDS) and 

115 self.field_contents_valid() 

116 ) 

117 

118 def total_score(self) -> int: 

119 return self.sum_fields(self.SCORED_FIELDS) 

120 

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

122 return [TrackerInfo( 

123 value=self.total_score(), 

124 plot_label="PCL total score", 

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

126 axis_min=self.MIN_SCORE - 0.5, 

127 axis_max=self.MAX_SCORE + 0.5 

128 )] 

129 

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

131 if not self.is_complete(): 

132 return CTV_INCOMPLETE 

133 return [CtvInfo( 

134 content=f"PCL total score {self.total_score()}" 

135 )] 

136 

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

138 return self.standard_task_summary_fields() + [ 

139 SummaryElement( 

140 name="total", 

141 coltype=Integer(), 

142 value=self.total_score(), 

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

144 SummaryElement( 

145 name="num_symptomatic", 

146 coltype=Integer(), 

147 value=self.num_symptomatic(), 

148 comment="Total number of symptoms considered symptomatic " 

149 "(meaning scoring 3 or more)"), 

150 SummaryElement( 

151 name="num_symptomatic_B", 

152 coltype=Integer(), 

153 value=self.num_symptomatic_b(), 

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

155 "(meaning scoring 3 or more)"), 

156 SummaryElement( 

157 name="num_symptomatic_C", 

158 coltype=Integer(), 

159 value=self.num_symptomatic_c(), 

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

161 "(meaning scoring 3 or more)"), 

162 SummaryElement( 

163 name="num_symptomatic_D", 

164 coltype=Integer(), 

165 value=self.num_symptomatic_d(), 

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

167 "(meaning scoring 3 or more)"), 

168 SummaryElement( 

169 name="ptsd", 

170 coltype=Boolean(), 

171 value=self.ptsd(), 

172 comment="Meets DSM-IV criteria for PTSD"), 

173 ] 

174 

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

176 n = 0 

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

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

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

180 n += 1 

181 return n 

182 

183 def num_symptomatic(self) -> int: 

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

185 

186 def num_symptomatic_b(self) -> int: 

187 return self.get_num_symptomatic(1, 5) 

188 

189 def num_symptomatic_c(self) -> int: 

190 return self.get_num_symptomatic(6, 12) 

191 

192 def num_symptomatic_d(self) -> int: 

193 return self.get_num_symptomatic(13, 17) 

194 

195 def ptsd(self) -> bool: 

196 num_symptomatic_b = self.num_symptomatic_b() 

197 num_symptomatic_c = self.num_symptomatic_c() 

198 num_symptomatic_d = self.num_symptomatic_d() 

199 return num_symptomatic_b >= 1 and num_symptomatic_c >= 3 and \ 

200 num_symptomatic_d >= 2 

201 

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

203 score = self.total_score() 

204 num_symptomatic = self.num_symptomatic() 

205 num_symptomatic_b = self.num_symptomatic_b() 

206 num_symptomatic_c = self.num_symptomatic_c() 

207 num_symptomatic_d = self.num_symptomatic_d() 

208 ptsd = self.ptsd() 

209 answer_dict = {None: None} 

210 for option in range(1, 6): 

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

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

213 q_a = "" 

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

215 # PCL-S 

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

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

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

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

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

221 q_a += subheading_spanning_two_columns( 

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

223 ) 

224 q_a += tr_qa( 

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

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

227 ) 

228 h = """ 

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

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

231 {tr_is_complete} 

232 {total_score} 

233 {num_symptomatic} 

234 {dsm_criteria_met} 

235 </table> 

236 </div> 

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

238 <tr> 

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

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

241 </tr> 

242 {q_a} 

243 </table> 

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

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

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

247 ≥2 ‘D’ symptoms. 

248 </div> 

249 """.format( 

250 CssClass=CssClass, 

251 tr_is_complete=self.get_is_complete_tr(req), 

252 total_score=tr_qa( 

253 f"{req.sstring(SS.TOTAL_SCORE)} (17–85)", 

254 score 

255 ), 

256 num_symptomatic=tr( 

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

258 answer(num_symptomatic_b) + ", " + 

259 answer(num_symptomatic_c) + ", " + 

260 answer(num_symptomatic_d) + " (" + answer(num_symptomatic) + ")" # noqa 

261 ), 

262 dsm_criteria_met=tr_qa( 

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

264 get_yes_no(req, ptsd) 

265 ), 

266 q_a=q_a, 

267 ) 

268 return h 

269 

270 

271# ============================================================================= 

272# PCL-C 

273# ============================================================================= 

274 

275class PclC(PclCommon, 

276 metaclass=PclMetaclass): 

277 """ 

278 Server implementation of the PCL-C task. 

279 """ 

280 __tablename__ = "pclc" 

281 shortname = "PCL-C" 

282 

283 TASK_TYPE = "C" 

284 

285 @staticmethod 

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

287 _ = req.gettext 

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

289 

290 

291# ============================================================================= 

292# PCL-M 

293# ============================================================================= 

294 

295class PclM(PclCommon, 

296 metaclass=PclMetaclass): 

297 """ 

298 Server implementation of the PCL-M task. 

299 """ 

300 __tablename__ = "pclm" 

301 shortname = "PCL-M" 

302 

303 TASK_TYPE = "M" 

304 

305 @staticmethod 

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

307 _ = req.gettext 

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

309 

310 

311# ============================================================================= 

312# PCL-S 

313# ============================================================================= 

314 

315class PclS(PclCommon, 

316 metaclass=PclMetaclass): 

317 """ 

318 Server implementation of the PCL-S task. 

319 """ 

320 __tablename__ = "pcls" 

321 shortname = "PCL-S" 

322 

323 event = Column( 

324 "event", UnicodeText, 

325 comment="Traumatic event" 

326 ) 

327 eventdate = Column( 

328 "eventdate", UnicodeText, 

329 comment="Date of traumatic event (free text)" 

330 ) 

331 

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

333 TASK_TYPE = "S" 

334 

335 @staticmethod 

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

337 _ = req.gettext 

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