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/pcl5.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 strseq 

33from semantic_version import Version 

34from sqlalchemy.ext.declarative import DeclarativeMeta 

35from sqlalchemy.sql.sqltypes import Boolean, Integer 

36 

37from camcops_server.cc_modules.cc_constants import CssClass 

38from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE 

39from camcops_server.cc_modules.cc_db import add_multiple_columns 

40from camcops_server.cc_modules.cc_html import ( 

41 answer, get_yes_no, subheading_spanning_two_columns, tr, tr_qa 

42) 

43from camcops_server.cc_modules.cc_request import CamcopsRequest 

44 

45from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

46from camcops_server.cc_modules.cc_task import ( 

47 get_from_dict, 

48 Task, 

49 TaskHasPatientMixin, 

50) 

51from camcops_server.cc_modules.cc_text import SS 

52from camcops_server.cc_modules.cc_trackerhelpers import ( 

53 equally_spaced_int, 

54 regular_tracker_axis_ticks_int, 

55 TrackerInfo, 

56 TrackerLabel, 

57) 

58 

59 

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

61# PCL-5 

62# ============================================================================= 

63 

64class Pcl5Metaclass(DeclarativeMeta): 

65 """ 

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

67 """ 

68 # noinspection PyInitNewSignature 

69 def __init__(cls: Type['Pcl5'], 

70 name: str, 

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

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

73 add_multiple_columns( 

74 cls, "q", 1, cls.N_QUESTIONS, 

75 minimum=0, maximum=4, 

76 comment_fmt="Q{n} ({s}) (0 not at all - 4 extremely)", 

77 comment_strings=[ 

78 "disturbing memories/thoughts/images", 

79 "disturbing dreams", 

80 "reliving", 

81 "upset at reminders", 

82 "physical reactions to reminders", 

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

84 "avoid activities/situations because they remind", 

85 "trouble remembering important parts of stressful event", 

86 "strong negative beliefs about self/others/world", 

87 "blaming", 

88 "strong negative emotions", 

89 "loss of interest in previously enjoyed activities", 

90 "feeling distant / cut off from people", 

91 "feeling emotionally numb", 

92 "irritable, angry and/or aggressive", 

93 "risk-taking and/or self-harming behaviour", 

94 "super alert/on guard", 

95 "jumpy/easily startled", 

96 "difficulty concentrating", 

97 "hard to sleep", 

98 ] 

99 ) 

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

101 

102 

103class Pcl5(TaskHasPatientMixin, Task, 

104 metaclass=Pcl5Metaclass): 

105 """ 

106 Server implementation of the PCL-5 task. 

107 """ 

108 __tablename__ = 'pcl5' 

109 shortname = 'PCL-5' 

110 provides_trackers = True 

111 extrastring_taskname = "pcl5" 

112 N_QUESTIONS = 20 

113 SCORED_FIELDS = strseq("q", 1, N_QUESTIONS) 

114 TASK_FIELDS = SCORED_FIELDS # may be overridden 

115 MIN_SCORE = 0 

116 MAX_SCORE = 4 * N_QUESTIONS 

117 

118 @staticmethod 

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

120 _ = req.gettext 

121 return _('PTSD Checklist, DSM-5 version') 

122 

123 # noinspection PyMethodParameters 

124 @classproperty 

125 def minimum_client_version(cls) -> Version: 

126 return Version("2.2.8") 

127 

128 def is_complete(self) -> bool: 

129 return ( 

130 self.all_fields_not_none(self.TASK_FIELDS) and 

131 self.field_contents_valid() 

132 ) 

133 

134 def total_score(self) -> int: 

135 return self.sum_fields(self.SCORED_FIELDS) 

136 

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

138 line_step = 20 

139 preliminary_cutoff = 33 

140 return [TrackerInfo( 

141 value=self.total_score(), 

142 plot_label="PCL-5 total score", 

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

144 axis_min=self.MIN_SCORE - 0.5, 

145 axis_max=self.MAX_SCORE + 0.5, 

146 axis_ticks=regular_tracker_axis_ticks_int( 

147 self.MIN_SCORE, 

148 self.MAX_SCORE, 

149 step=line_step 

150 ), 

151 horizontal_lines=equally_spaced_int( 

152 self.MIN_SCORE + line_step, 

153 self.MAX_SCORE - line_step, 

154 step=line_step 

155 ) + [preliminary_cutoff], 

156 horizontal_labels=[ 

157 TrackerLabel(preliminary_cutoff, 

158 self.wxstring(req, "preliminary_cutoff")), 

159 ] 

160 )] 

161 

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

163 if not self.is_complete(): 

164 return CTV_INCOMPLETE 

165 return [CtvInfo( 

166 content=f"PCL-5 total score {self.total_score()}" 

167 )] 

168 

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

170 return self.standard_task_summary_fields() + [ 

171 SummaryElement( 

172 name="total", 

173 coltype=Integer(), 

174 value=self.total_score(), 

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

176 SummaryElement( 

177 name="num_symptomatic", 

178 coltype=Integer(), 

179 value=self.num_symptomatic(), 

180 comment="Total number of symptoms considered symptomatic " 

181 "(meaning scoring 2 or more)"), 

182 SummaryElement( 

183 name="num_symptomatic_B", 

184 coltype=Integer(), 

185 value=self.num_symptomatic_b(), 

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

187 "(meaning scoring 2 or more)"), 

188 SummaryElement( 

189 name="num_symptomatic_C", 

190 coltype=Integer(), 

191 value=self.num_symptomatic_c(), 

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

193 "(meaning scoring 2 or more)"), 

194 SummaryElement( 

195 name="num_symptomatic_D", 

196 coltype=Integer(), 

197 value=self.num_symptomatic_d(), 

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

199 "(meaning scoring 2 or more)"), 

200 SummaryElement( 

201 name="num_symptomatic_E", 

202 coltype=Integer(), 

203 value=self.num_symptomatic_e(), 

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

205 "(meaning scoring 2 or more)"), 

206 SummaryElement( 

207 name="ptsd", 

208 coltype=Boolean(), 

209 value=self.ptsd(), 

210 comment="Provisionally meets DSM-5 criteria for PTSD"), 

211 ] 

212 

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

214 n = 0 

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

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

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

218 n += 1 

219 return n 

220 

221 def num_symptomatic(self) -> int: 

222 return self.get_num_symptomatic(1, self.N_QUESTIONS) 

223 

224 def num_symptomatic_b(self) -> int: 

225 return self.get_num_symptomatic(1, 5) 

226 

227 def num_symptomatic_c(self) -> int: 

228 return self.get_num_symptomatic(6, 7) 

229 

230 def num_symptomatic_d(self) -> int: 

231 return self.get_num_symptomatic(8, 14) 

232 

233 def num_symptomatic_e(self) -> int: 

234 return self.get_num_symptomatic(15, 20) 

235 

236 def ptsd(self) -> bool: 

237 num_symptomatic_b = self.num_symptomatic_b() 

238 num_symptomatic_c = self.num_symptomatic_c() 

239 num_symptomatic_d = self.num_symptomatic_d() 

240 num_symptomatic_e = self.num_symptomatic_e() 

241 return ( 

242 num_symptomatic_b >= 1 and 

243 num_symptomatic_c >= 1 and 

244 num_symptomatic_d >= 2 and 

245 num_symptomatic_e >= 2 

246 ) 

247 

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

249 score = self.total_score() 

250 num_symptomatic = self.num_symptomatic() 

251 num_symptomatic_b = self.num_symptomatic_b() 

252 num_symptomatic_c = self.num_symptomatic_c() 

253 num_symptomatic_d = self.num_symptomatic_d() 

254 num_symptomatic_e = self.num_symptomatic_e() 

255 ptsd = self.ptsd() 

256 answer_dict = {None: None} 

257 for option in range(5): 

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

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

260 q_a = "" 

261 

262 section_start = { 

263 1: 'B (intrusion symptoms)', 

264 6: 'C (avoidance)', 

265 8: 'D (negative cognition/mood)', 

266 15: 'E (arousal/reactivity)' 

267 } 

268 

269 for q in range(1, self.N_QUESTIONS + 1): 

270 if q in section_start: 

271 section = section_start[q] 

272 q_a += subheading_spanning_two_columns( 

273 f"DSM-5 section {section}" 

274 ) 

275 

276 q_a += tr_qa( 

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

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

279 ) 

280 

281 h = """ 

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

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

284 {tr_is_complete} 

285 {total_score} 

286 {num_symptomatic} 

287 {dsm_criteria_met} 

288 </table> 

289 </div> 

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

291 <tr> 

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

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

294 </tr> 

295 {q_a} 

296 </table> 

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

298 [1] Questions with scores ≥2 are considered symptomatic; see 

299 https://www.ptsd.va.gov/professional/assessment/adult-sr/ptsd-checklist.asp 

300 [2] ≥1 ‘B’ symptoms and ≥1 ‘C’ symptoms and ≥2 ‘D’ symptoms 

301 and ≥2 ‘E’ symptoms. 

302 </div> 

303 """.format( # noqa 

304 CssClass=CssClass, 

305 tr_is_complete=self.get_is_complete_tr(req), 

306 total_score=tr_qa( 

307 f"{req.sstring(SS.TOTAL_SCORE)} (0–80)", 

308 score 

309 ), 

310 num_symptomatic=tr( 

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

312 answer(num_symptomatic_b) + ", " + 

313 answer(num_symptomatic_c) + ", " + 

314 answer(num_symptomatic_d) + ", " + 

315 answer(num_symptomatic_e) + " (" + answer(num_symptomatic) + ")" # noqa 

316 ), 

317 dsm_criteria_met=tr_qa( 

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

319 get_yes_no(req, ptsd) 

320 ), 

321 q_a=q_a, 

322 ) 

323 return h